About Thread-Safety of Weak_Ptr

Is std::weak_ptrT::lock thread-safe?

The standard explicitly says that weak_ptr::lock is "executed atomically". So that answers 1 and 3.

For #2, if you're asking about assigning to the same weak_ptr, then it's a data race. Operations that change a shared state's use_count don't provoke a data race, but copying or manipulating the weak_ptr itself is doing more than just poking at use_count.

But if you're talking about locking one weak_ptr while nulling out a different weak_ptr that are both talking to the same shared state, that's fine. The two only interact through the shared state's count, which is stated to be fine.

And yes, atomic<weak_ptr<T>> would allow you to manipulate the same object from multiple threads.

How `weak_ptr` and `shared_ptr` accesses are atomic

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
}
}

weak_ptr to singleton not thread-safe

The assignment under lock will be visible to any other thread checking the weak_ptr under lock.

The reason multiple instances exist is that the destructor order of operations doesn't guarantee otherwise. !weak.lock() isn't equivalent to "with certainty, there are no instances of this object". It is equivalent to the inverse of "with certainty, there is an instance of this object". There may be an instance, because the reference count is decremented before the deleter has a chance to act.

Imagine this sequence:

  • Thread 1: ~shared_ptr is called.
  • Thread 1: The strong reference count is decremented.
  • Thread 1: It compares equal to zero, and the deleter begins.
  • Thread 2: get_singleton is called.
  • Thread 2: The lock is acquired.
  • Thread 2: !weak.lock() is satisfied, so we construct a new instance.
  • Thread 1: ... meanwhile, we are waiting to acquire the lock ...
  • Thread 2: The new instance is constructed, and assigned, and the lock is released.
  • Thread 1: Now, the deleter can acquire the lock, and delete the original instance.

EDIT:

To accomplish this, you need some indicator that there is no instance. That must be reset only after the instance is destroyed. And second, you need to decide what to do if you encounter this case where an instance exists but is being destroyed:

template <typename T>
std::shared_ptr<T> get_singleton() {
static mutex mtx;
static weak_ptr<T> weak;
static bool instance;

while (true) {
scoped_lock lk(mtx);

shared_ptr<T> shared = weak.lock();
if (shared)
return shared;

if (instance)
// allow deleters to make progress and try again
continue;

instance = true;
shared.reset(new T, [](T* ptr){
scoped_lock lk(mtx);
delete ptr;
instance = false;
});
weak = shared;
return shared;
}
}

To put it a different way, there are these states in the state machine:

  1. no T exists
  2. T is being constructed, but can't yet be accessed
  3. T exists and is accessible
  4. T is being destroyed, and can no longer be accessed

The bool lets us know that we are in state #4. One approach is to loop until we are no longer in state #4. We drop the lock so that deleters can make progress. When we retake the lock, another thread may have swooped in and created the instance, so we need to start at the top and re-check everything.

Can multi threads access same weak_ptr object C++

There is no issue calling lock. lock isn't like lock on a mutex where it blocks all other threads. What it does do is effectively lock you into the ownership of the pointer if there is still a valid shared_ptr some where in your code at that time.

That means the only thing you need to do is check if wPtr is not equal to nullptr before you use it since lock could return a null shared_ptr.


Do note that this does not provide any thread safety guarantee about the object pointed to by the pointer. You still have to make sure you access that in a thread safe manner. Without knowing what you are doing I can't say if your loops are actualy safe.

Question about the thread-safety of using shared_ptr

std::shared_ptr<T> guarantees that access to its control block is thread-safe, but not access to the std::shared_ptr<T> instance itself, which is generally an object with two data members: the raw pointer (the one returned by get()) and the pointer to the control block.

In your code, the same std::shared_ptr<int> instance may be concurrently accessed by the two threads; f1 reads, and f2 writes.

If the two threads were accessing two different shared_ptr instances that shared ownership of the same object, there would be no data race. The two instances would have the same control block, but accesses to the control block would be appropriately synchronized by the library implementation.

If you need concurrent, race-free access to a single std::shared_ptr<T> instance from multiple threads, you can use std::atomic<std::shared_ptr<T>>. (There is also an older interface that can be used prior to C++20, which is deprecated in C++20.)

Are smart pointers thread safe?

Various different smart pointer objects provide various different degrees of thread safety. You have to check the documentation for the individual implementation carefully to see what level of thread safety it provides.

The most common question is specifically about std::shared_ptr and std::weak_ptr. These provide standard thread safety for individual pointer instances. That is, one thread must not access a shared_ptr or weak_ptr while another thread is, or might be, modifying that exact same shared_ptr or weak_ptr object. However, they provide full thread safety for distinct pointers that reference the same object. So one thread can modify a shared_ptr while another thread is accessing a shared_ptr to that same underlying object whose lifetime is managed by the smart pointers.

Probable bug in weak_ptr passed to a thread

Those implementations copy the shared_ptr. i.e. They copy the argument and tie it to the lifetime of the thread. Copying is the default for argument binding with thread creation.

This wouldn't be considered a bug unless the standard said the implementation couldn't do that.

As far as getting the behavior you want, I think you'd have the same problem if you used a lambda. Instead you could use a functor, or you could use some global or static variables.

Here's your example converted to use a functor. The benefit is that you control the member variables. The downside is the boilerplate.

#include <iostream>
#include <string>
#include <thread>
#include <chrono>

class TestWeakPtrAlive
{
std::weak_ptr<int> m_pw;

public:
TestWeakPtrAlive(std::weak_ptr<int> pw)
: m_pw(std::move(pw))
{}

void operator()()
{
// wait for main thread to delete pw
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
std::shared_ptr<int> p = m_pw.lock();
std::cout << (p ? "pw alive" : "pw dead")
<< std::endl;
}
};

int main()
{
std::shared_ptr<int> p = std::make_shared<int>(18);
auto functor = TestWeakPtrAlive(p);
std::thread t(std::move(functor));
p = nullptr;
t.join();
return 0;
}

Edit: I just thought of a fairly obvious solution which would be to just create a weak_ptr and pass that to the thread constructor.

#include <iostream>
#include <string>
#include <thread>
#include <chrono>

void TestWeakPtrAlive(std::weak_ptr<int> pw)
{
// wait for main thread to delete pw
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
std::shared_ptr<int> p = pw.lock();
std::cout << (p ? "pw alive" : "pw dead")
<< std::endl;
}

int main()
{
std::shared_ptr<int> p = std::make_shared<int>(18);
std::weak_ptr<int> wp = p;
std::thread t(TestWeakPtrAlive, std::move(wp));
p = nullptr;
t.join();
return 0;
}


Related Topics



Leave a reply



Submit