How the references "weak_ptr" and "shared_ptr" are atomic
std::shared_ptr<int> int_ptr; int main() { int_ptr = std::make_shared<int>(1); std::thread th{[&]() { std::weak_ptr int_ptr_weak = int_ptr; auto int_ptr_local = int_ptr_weak.lock(); if (int_ptr_local) { cout << "Value in the shared_ptr is " << *int_ptr_local << endl; } }); int_ptr.reset(nullptr); th.join(); return 0; }
Is code safe over the stream? I read this answer About weak_ptr streaming security , but just wanted to make sure the code above is thread safe.
The reason I'm asking about this is because if the code above is really thread safe, I don’t understand how the std::weak_ptr
and std::shared_ptr
interfaces perform the following atomic expired() ? shared_ptr<T>() : shared_ptr<T>(*this)
operation expired() ? shared_ptr<T>() : shared_ptr<T>(*this)
expired() ? shared_ptr<T>() : shared_ptr<T>(*this)
. It just seems to me that creating two logical lines of code like the above cannot be made synchronous without using any mutex or spin lock.
I understand how atomic increments work with different instances of shared pointers, and I understand that shared_ptr
alone is not thread safe, but if the above is really thread safe, it is very similar to thread safe shared_ptr
, and I don’t understand how the two lines of code as in the conditional above, can be made by atoms without blocking.
This question has two parts:
Thread safety
The code is NOT thread safe, but it has nothing to do with lock()
:
A race exists between int_ptr.reset();
and std::weak_ptr int_ptr_weak = int_ptr;
. Since one thread modifies the non-atomic variable int_ptr
, and the other reads it, which by definition is the calculation of data.
So this will be OK:
int main() { auto int_ptr = std::make_shared<int>(1); std::weak_ptr<int> int_ptr_weak = int_ptr; //create the weak pointer in the original thread std::thread th( [&]() { auto int_ptr_local = int_ptr_weak.lock(); if (int_ptr_local) { std::cout << "Value in the shared_ptr is " << *int_ptr_local << std::endl; } }); int_ptr.reset(); th.join(); }
Atomic code version of the example expired() ? shared_ptr<T>() : shared_ptr<T>(*this)
expired() ? shared_ptr<T>() : shared_ptr<T>(*this)
Of course, the whole process cannot be atomic. In fact, the important part is that a strong number of links only increases if it is already greater than zero and that verification and increment occur in atomic mode. I don't know if there are any primitives available for the system / architecture, but one way to implement it in C ++ 11 is:
std::shared_ptr<T> lock() { if (!isInitialized) { return std::shared_ptr<T>(); } std::atomic<int>& strong_ref_cnt = get_strong_ref_cnt_var_from_control_block(); int old_cnt = strong_ref_cnt.load(); while (old_cnt && !strong_ref_cnt.compare_exchange_weak(old_cnt, old_cnt + 1)) { ; } if (old_cnt > 0) { // create shared_ptr without touching the control block any further } else { // create empty shared_ptr } }
Is code safe over the stream?
I believe this is not the case, since int_ptr.reset(nullptr);
chases against std::weak_ptr int_ptr_weak = int_ptr;
I don’t understand how the std :: weak_ptr and std :: shared_ptr interfaces perform the following atomic
expired() ? shared_ptr<T>() : shared_ptr<T>(*this)
operationexpired() ? shared_ptr<T>() : shared_ptr<T>(*this)
expired() ? shared_ptr<T>() : shared_ptr<T>(*this)
Such an operation is not atomic, since expired()
can return false, but by the time you act on this value, it may be more inaccurate. On the other hand, if it returns true, this ensures that it remains accurate if no one has modified this particular instance of shared_ptr before. That is, operations with other copies of this shared_ptr cannot lead to its failure.
The implementation of weak_ptr::lock()
will not use expired()
. He will probably do something like atomic comparison, where an extra strong link is added only if the current number of strong links is greater than zero.