22
std::shared_ptr<int> g_s = std::make_shared<int>(1);
void f1()
{
    std::shared_ptr<int>l_s1 = g_s; // read g_s
}

void f2()
{
    std::shared_ptr<int> l_s2 = std::make_shared<int>(3);
    std::thread th(f1);
    th.detach();
    g_s = l_s2; // write g_s
}

Regarding the code above, I know different threads reading and writing the same shared_ptr leads to race conditions. But how about weak_ptr? Is there any race condition in the code below? (My platform is Microsoft VS2013.)

std::weak_ptr<int> g_w;

void f3()
{
    std::shared_ptr<int>l_s3 = g_w.lock(); //2. here will read g_w
    if (l_s3)
    {
        ;/.....
    }
}

void f4()
{
    std::shared_ptr<int> p_s = std::make_shared<int>(1);
    g_w = p_s;

    std::thread th(f3);
    th.detach();
    // 1. p_s destory will motify g_w (write g_w)
}
Craig M. Brandenburg
  • 3,354
  • 5
  • 25
  • 37
Leonhart Squall
  • 810
  • 1
  • 7
  • 15
  • 1
    Yes, whether l_s3 is nullptr is entirely arbitrary. The more cores your processor has, the more likely that f3() starts running early so the less likely it is nullptr. This is a standard threading race bug, otherwise entirely unrelated to the weak_ptr<> implementation. – Hans Passant Dec 20 '13 at 14:51
  • 1
    It's actually a bit worse than that: programs with data races have undefined behavior. In a typical implementation the read of `g_w` in `f3` will see either the value `nullptr` or `p_s` - but that behavior is not guaranteed by the standard. In particular, an implementation upon which normal single-word writes are non-atomic could return a "torn" value that is party the old and part new value. – Casey Dec 20 '13 at 15:11
  • 2
    In no way a race condition is a bug. a weak pointer may be null, if you are fast enough to grab it then good for you. if not, then too bad, but this wont cause a bug in the software. when you use a weak ref, both paths (lock success and fail) need to be made sure that they are valid paths. if you have a bug in either case, it has nothing to do with the race condition, its just a flaw. if you need it be lockable at anytime make it shared. – v.oddou Aug 04 '14 at 01:27
  • The visible code of `f3()` and `f4()` is fine. However, the commented code is NOT fine: `// 1. p_s destroy will modify g_w (write g_w)`. Updating `g_w` in `f4()` and at the same time reading `g_w` via `g_w.lock()` in `f3()` is undefined behaviour! – Kai Petzke Apr 21 '20 at 13:42
  • @KaiPetzke I think the comment in `f4` is wrong.. `p_s` going out of scope will not affect `g_w`, they're different objects. Only the control-block will be adjusted – LWimsey Jan 02 '22 at 20:32

5 Answers5

47

I know I'm late, but this comes up when searching for "weak_ptr thread", and Casey's answer just isn't the whole truth. Both shared_ptr and weak_ptr can be used from threads without further synchronization.

For shared_ptr, there's a lot of documentation (e.g. on cppreference.com or on stackoverflow). You can safely access shared_ptr's that point to the same object from different threads. You just can't bang on the same pointer from two threads. In other words:

// Using p and p_copy from two threads is fine.
// Using p from two threads or p and p_ref from two threads is illegal.
std::shared_ptr<A> p = std::make_shared<A>();
std::shared_ptr<A> &p_ref = p;
std::shared_ptr<A> p_copy = p;

To solve that problem in your code, pass g_s as parameter (by value)* to f1().

For weak pointers, the safety guarantee is hidden in the documentation for weak_ptr::lock:

Effectively returns expired() ? shared_ptr<T>() : shared_ptr<T>(*this), executed atomically.

You can use weak_ptr::lock() to get a shared_ptr from other threads without further synchronization. This is also confirmed here for Boost and in this SO answer by Chris Jester-Young.

Again, you have to make sure not to modify the same weak_ptr from one thread while accessing it from another, so pass g_w into f3() by value as well.

Community
  • 1
  • 1
Christian Aichinger
  • 6,989
  • 4
  • 40
  • 60
  • 9
    Did you not notice that the examples in the OP involve a `shared_ptr` or `weak_ptr` object that is being read from one thread and written in the other without synchronization? Both examples have undefined behavior due to data races. – Casey May 15 '15 at 20:22
  • 1
    @Casey: I did indeed miss that, that's exactly the thing cppreference.com link says you can't do. I updated the answer. Thanks! – Christian Aichinger May 15 '15 at 20:39
  • I seem to have found contradicting documentation. The Boost and SO answer you linked are for boost shared/weak ptr seem to confirm your conclusion for boost but not std (which was in the question). The std documentation states for `expired()`: This function is inherently racy if the managed object is shared among threads. In particular, a false result may become stale before it can be used. A true result is reliable. So the std::weak_ptr<>::lock() is racy if it was implemented to use expired() and has no other synchronization mechanisms. – Erroneous May 21 '20 at 15:09
  • 1
    @Erroneous: `lock()` is thread-safe in the way the answer describes. It does *not* use `expired()` internally. `expired()` is an additional convenience function to test whether the weak pointer is still valid. Since it doesn't acquire the `shared_ptr`, it has no way to prevent the object from being deleted by the time the call returns. It might be best to avoid it. This type of race condition does not affect `lock()` since that function *does* acquire a `shared_ptr`, thus ensuring that the object stays alive. – Christian Aichinger May 21 '20 at 19:04
  • @ChristianAichinger ah it seems I missed the key phrase *executed atomically*, which disallows an implementation from using the code given for what it effectively returns. – Erroneous May 21 '20 at 19:11
  • @ChristianAichinger What do you mean by you just can't ***bang on the same pointer*** from two threads? – John May 21 '22 at 04:53
  • A good post for understanding ***Using p and p_copy from two threads is fine, whereas using p from two threads or p and p_ref from two threads is illegal.*** is seen at https://stackoverflow.com/q/72327077/13611002 – John May 21 '22 at 06:28
8

For brevity in the following discussion, different weak_ptrs and shared_ptrs that all are generated from the same original shared_ptr or unique_ptr will be termed 'instances'. weak_ptrs and shared_ptrs that don't share the same object do not need to be considered in this analysis. The general rules for assessing thread safety are:

  1. Simultaneous const member function calls on the same instance are thread-safe. All observer functions are const.
  2. Simultaneous calls on different instances are thread-safe, even when one of the calls is a modifier.
  3. Simultaneous calls on the same instance when at least one of the calls is a modifier are not thread-safe.

The following table shows thread-safety when two threads are operating on the same instance at the same time.

+---------------+----------+-------------------------+------------------------+
|   operation   |   type   | other thread modifying  | other thread observing |
+---------------+----------+-------------------------+------------------------+
| (constructor) |          | not applicable          | not applicable         |
| (destructor)  |          | unsafe                  | unsafe                 |
| operator=     | modifier | unsafe                  | unsafe                 |
| reset         | modifier | unsafe                  | unsafe                 |
| swap          | modifier | unsafe                  | unsafe                 |
| use_count     | observer | unsafe                  | safe                   |
| expired       | observer | unsafe                  | safe                   |
| lock          | observer | unsafe                  | safe                   |
| owner_before  | observer | unsafe                  | safe                   |
+---------------+----------+-------------------------+------------------------+

The cppreference discussion of std::atomic(std::weak_ptr) is clearest on the safety of simultaneous accesses to different instances:

Note that the control block used by std::weak_ptr and std::shared_ptr is thread-safe: different non-atomic std::weak_ptr objects can be accessed using mutable operations, such as operator= or reset, simultaneously by multiple threads, even when these instances are copies or otherwise share the same control block internally.

C++20 introduces a std::atomic specialization of weak pointer that provides thread-safe modification of the same instance through appropriate synchronization. Note that when it comes to constructors, initialization from another instance is not atomic. For example, atomic<weak_ptr<T>> myptr(anotherWeakPtr); is not an atomic operation.

rsjaffe
  • 5,600
  • 7
  • 27
  • 39
6

shared_ptr and weak_ptr fall under the same blanket threadsafety requirements as all other standard library types: simultaneous calls to member functions must be threadsafe if those member functions are non-modifying (const) (Detailed in C++11 §17.6.5.9 Data Race Avoidance [res.data.races]). Assignment operators are notably not const.

Casey
  • 41,449
  • 7
  • 95
  • 125
  • There are stronger thread safety guarantees on ``std::shared_ptr`` and ``std::weak_ptr`` than the blanket statements for all STL containers, see my answer. – Christian Aichinger May 06 '15 at 18:59
  • 1
    @ChristianAichinger The only "additional thread safety guarantees" for `shared_ptr` & `weak_ptr` are there to ensure that changes to the hidden reference count from multiple threads do not introduce data races. Individual `shared_ptr`/`weak_ptr` objects have no special protection against data races. – Casey May 15 '15 at 20:30
  • Indeed, but that's a pretty useful guarantee in practice, and not even mentioning it in a "About threadsafe of weak_ptr" question misses something important. Add that and I'll un-downvote (can't right now due to lock-in). I was just a bit disappointed coming here via googling the title question and finding this answer not very helpful, to be honest. – Christian Aichinger May 15 '15 at 20:45
  • 1
    @ChristianAichinger If this were a general question about threadsafety of `shared_ptr` and friends, I would agree. But despite the general-sounding title, the question is very specific: "I know I can't do with `shared_ptr`, can I do with `weak_ptr`?" My answer addresses that question exactly, and points out that won't work with `weak_ptr` for the same reason it does not work with `shared_ptr`. Discussing every single aspect of threadsafety wrt. `shared_ptr` would only confuse the issue. – Casey May 18 '15 at 15:35
  • @ChristianAichinger "_There are stronger thread safety guarantees_" Actually, no, there isn't. There is an explicit description of the fact that concurrent modification of the use count is not a race condition. It was not in the first place. – curiousguy Jan 20 '17 at 00:04
0

So to clear it out for me, I am still not quite sure what happens if reset() is called on a std::shared_ptr, at the same time lock() is called for a std:weak_ptr.

Extremely simplified like this:

std::shared_ptr<Object> sharedObject;
std::weak_ptr<Object> weakObject = sharedObject;

void thread1()
{
    std::shared_ptr<Object> leaseObject = weakObject.lock(); //weakObject is bound to sharedObject
}

void thread2()
{
    sharedObject.reset();
}

We assume sharedObject is not sharing its pointer with any other objects.

If the both commands (reset() and lock() ) happens at the exactly same time, I expect that either:

  • The sharedObject is successfully reset and weakObject.lock() returns nullptr, and the sharedObject is deleted from the memory.

  • The sharedObject is successfully reset and weakObject.lock() returns a pointer to sharedObject. When leaseObject looses scope, the sharedObject is deleted from the memory.

If the above code is undefined, I have to replace std:mutex in the Object class I have, to outside of the class, but that may be another question in another thread. ;)

  • Your example looks fine to me, and behaves exactly, as you described. Of course, you have to make sure, that the first two lines of code have been executed fully, before either thread1() or thread2() are called. "fully" means here in particular, that a memory fencing instruction sits between the initialization and the call to these functions. Standard synchronisation functions like std::mutex::lock() or std::thread() will issue those fences for you. – Kai Petzke Apr 21 '20 at 13:34
0

Using weak_ptr and shared_ptr across threads is safe; the weak_ptr/shared_ptr objects themselves aren't thread-safe. You can't read/write to a single smart pointer across threads.

Access to g_s like you did isn't safe, be it a shared_ptr or a weak_ptr.

jyavenard
  • 2,142
  • 1
  • 26
  • 35