How `weak_ptr` and `shared_ptr` accesses are atomic

2.4k views Asked by At
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 the code above thread safe? I read this answer About thread-safety of weak_ptr but just wanted to make sure that the above code is thread safe.

The reason I ask this is that if the code above is indeed thread safe, I cannot understand how the std::weak_ptr and std::shared_ptr interfaces make the following operation atomic expired() ? shared_ptr<T>() : shared_ptr<T>(*this). It just seems to me that making two logical lines of code like above cannot be made synchronous without using some sort of mutex or spinlock.

I understand how atomic increments work with different instances of shared pointers and I understand that shared_ptrs themselves are not thread safe, but if the above is indeed thread safe it is very much like a thread safe shared_ptr and I don't understand how two lines of code like in the conditional above can be made atomic without locks.

4

There are 4 answers

3
MikeMB On BEST ANSWER

This question has two parts:

Thread-safety

The code is NOT threadsafe, but this has nothing to do with lock():
The race exists between int_ptr.reset(); and std::weak_ptr int_ptr_weak = int_ptr;. Because one thread is modifying the non-atomic variable int_ptr while the other reads it, which is - by definition - a data race.

So this would 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 version of the example code expired() ? shared_ptr<T>() : shared_ptr<T>(*this)

Of course the whole process can't be atomic. The actually important part is that the strong ref count is only incremented if it is already greater than zero and that the check and the increment happen in an atomic fashion. I don't know if there are any system/architecture specific primitives available for this, but one way to implement it in c++11 would be:

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
    }
}
12
Joseph Artsimovich On

Is the code above thread safe?

I believe it's not, as int_ptr.reset(nullptr); is racing against std::weak_ptr int_ptr_weak = int_ptr;

I cannot understand how the std::weak_ptr and std::shared_ptr interfaces make the following operation atomic expired() ? shared_ptr<T>() : shared_ptr<T>(*this)

Such an operation is not atomic, as expired() may return false, yet by the time you act upon that value, it may no longer be accurate. On the other hand if it returns true, that's guaranteed to remain accurate, as long as no one modified this particular instance of shared_ptr since then. That is, operations on other copies of a given shared_ptr can't cause it to unexpire.

The weak_ptr::lock() implementation is not going to be using expired(). It will probably do something like atomic compare-exchange, where an extra strong reference is added only if the current number of strong references is greater than zero.

2
Wil Evers On

No, your code is not thread-safe. There is a data race between the int_ptr.reset() operation in the main thread (which is a write operation) and the initialization of int_weak_ptr from int_ptr in th (which is a read operation).

2
MSalters On

" how the std::weak_ptr and std::shared_ptr interfaces make the following operation atomic expired() ? shared_ptr<T>() : shared_ptr<T>(*this)"

The interfaces don't. It's internal to the implementation. Exactly how it's done will differ between implementations.