Search code examples
c++multithreadingweak-ptrc++20

Is std::weak_ptr<T>::lock thread-safe?


Below is some sample code showing my use case. I have a PIMPL where the implementation can be shared (it's just a bunch of expensively-produced data) but the implementation can be destroyed when it's no longer needed. An instance of the class HasImpl uses a shared pointer to Impl, and the class definition includes a static weak_ptr to Impl that acts as a "pointer dispenser" to new instances of HasImpl, giving them a handle to the Impl if one exists already.

The sample has two alternatives for calling weak_ptr::lock--one that assumes that the the answer to questions 1-3 below are all "yes", and another that does not. The only reason I'd prefer that weak_ptr::lock is thread-safe is that there could be multiple threads trying to get a copy of the pointer to Impl, and if lock is thread-safe, most threads of execution won't have to pass a static variable definition (where the thread would have to check to see if it were already initialized) and won't have to compete to acquire a mutex.

/* In HasImpl.h */
class HasImpl {
public:
  HasImpl();
private:
  class Impl;
  static std::weak_ptr<Impl> sharedImplDispenser;
  std::shared_ptr<Impl> myPtrToSharedImpl;
}

/* In HasImpl.cpp */
class HasImpl::Impl {
public:
  Impl(); //constructor that takes a lot of time to run
  //Lots of stuff, expensively produced, accessable to HasImpl through a shared_ptr to Impl
}

/* hypothetical constructor if weak_ptr::lock is thread-safe */
HasImpl::HasImpl() : myPtrToSharedImpl{sharedImplDispenser.lock()}
{
  if (!myPtrToSharedImpl) {
    static std::mutex mtx;
    std::lockguard<std::mutex> lck(mtx);
    myPtrToSharedImpl = sharedImplDispenser.lock();
    if (!myPtrToSharedImpl) {
      const auto new_impl{std::make_shared<Impl()};
      sharedImplDispenser = new_impl; // the only place in the program where a value is assigned to sharedImplDispenser
      myPtrToSharedImpl = new_impl;
    }
  }
}

/* hypothetical constructor if weak_ptr::lock is not thread-safe */
HasImpl::HasImpl()
{
  static std::mutex mtx;
  std::lockguard<std::mutex> lck(mtx);
  myPtrToSharedImpl = sharedImpl.lock();
  if (!myPtrToSharedImpl) {
    const auto new_impl{std::make_shared<Impl()};
    sharedImplDispenser = new_impl; // the only place in the program where a value is assigned to sharedImplDispenser
    myPtrToSharedImpl = new_impl;
  }
} 
  1. Assuming that std::weak_ptr is not empty and was assigned a pointer sometime in the distant past, will the control block be ok if one thread calls weak_ptr::lock while another thread may be calling weak_ptr::lock?
  2. Is calling weak_ptr::lock while another thread may be assigning a ptr to an empty weak_ptr safe enough? That is, will the value either return nullptr or the new pointer? I don't care if the nullptr is spurious (that is, that assignment has occurred but the other threads don't know about it yet). I just don't want to corrupt the control block or obtain an invalid pointer value from the call.
  3. Is calling weak_ptr::lock while the last shared_ptr to the object is being destroyed thread safe?
  4. If there are problems with 1 through 3, will std::atomic<std::weak_ptr<T>> in C++20 fix the issue?

Solution

  • 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.