0

As it's well known that shared_ptr only guarantees access to underlying control block is thread safe and no guarantee made for accesses to owned object.

Then why there is a race condition in the code snippet below:

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
}

In the code snippet above, the owned object of the shared pointer named g_s are not accessed indeed.

I am really confused now. Could somebody shed some light on this matter?

John
  • 2,963
  • 11
  • 33
  • At the very least, there is nothing guaranteeing that `std::shared_ptrl_s1 = g_s;` will run before or after `g_s = l_s2;`, so there is no way to know if `l_s1` will be assigned a pointer to an `int` holding 1 or an `int` holding 3. – user4581301 May 21 '22 at 02:02
  • The sequencing order of the code in `f1()` (i.e. `std::shared_ptr l_si = g_s`) and the last statement in `f2()` (i.e. `g_s = l_s2`) are indeterminate. Creating and detaching `th` allows `th` to continue executing without any guarantee of sequencing between statements in the `f1()` and `f2()`. There is nothing in your code preventing the two threads (or statements in them, since they are not atomic) preempting each other and running in arbitrary sequence (or overlapping in time). – Peter May 21 '22 at 02:07
  • @Peter If I understand you correctly, the main problem is the same `shared_ptr` instance named `g_s` may be may be concurrently accessed by the two threads. Both `std::shared_ptrl_s1 = g_s`; and `g_s = l_s2;` may be called at the same time. So both the control block and the raw pointer pointed to the managed object may be read and written at the same time, which may cause the data race. Am I right? – John May 21 '22 at 02:19
  • @user4581301 Sorry, I can't agree with. The problem is not there is no way to know if `l_s1` will be assigned a pointer to an int holding 1 or an int holding 3. The problem is with `g_s`. The main problem is the same `shared_ptr` instance named `g_s` may be may be concurrently accessed by the two threads. Both `std::shared_ptrl_s1 = g_s`; and `g_s = l_s2;` may be called at the same time. So both the control block and the raw pointer pointed to the managed object may be read and written at the same time, which may cause the data race. How do you think about it? – John May 21 '22 at 02:21
  • My point is even if the gods smile and there is no simultaneous access, you can't be sure which access will happen first. Protect both accesses with a mutex so that you know there will be no simultaneous access and you still can't know the ordering. – user4581301 May 21 '22 at 02:22
  • @John - More or less. An object cannot protect itself if accessed//modified concurrently by two threads. Each bit of code which does the access must ensure synchronisation. Common mistakes people make are to assume there is some "implied" ordering, or that individual non-atomic statements cannot be interrupted by another thread when partway complete. – Peter May 21 '22 at 02:23
  • @Peter If I understand you correctly, even simple like `int num=0; std::thread([&](){std::cout << num << std::endl;}).detach(); std::thread([&](){num++;}).detach();` (https://godbolt.org/z/4EbYfroed) has race condition. Am I right? – John May 21 '22 at 02:37
  • @John Yes. Accessing the value of `num` in the first thread is unsequenced relative to the accessing, incrementing, and storing of `num` in the second thread. – Peter May 21 '22 at 13:34

1 Answers1

2

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

Brian Bi
  • 111,498
  • 10
  • 176
  • 312
  • I didn't know about the thread safety of the control block. Do you have a reference? – Mark Ransom May 21 '22 at 02:05
  • @MarkRansom *"In your code, **the same std::shared_ptr instance** may be concurrently accessed by the two threads; f1 reads, and f2 writes."* If I understand you correctly, the same `shared_ptr` instance named `g_s` may be may be concurrently accessed by the two threads(i.e. `std::shared_ptrl_s1 = g_s;` and `g_s = l_s2; `) may can at the same time. **So both the control block and the raw pointer pointed to the managed object may be read and written at the same time, which may cause the data race**. Am I right? – John May 21 '22 at 02:12
  • 1
    @MarkRansom http://eel.is/c++draft/util.sharedptr#util.smartptr.shared.general-5 – Brian Bi May 21 '22 at 02:18
  • @John In your code, both the raw pointer and the *pointer to* the control block may be concurrently read from and written to by different threads, which would constitute a race. The control block may also be concurrently accessed but it doesn't matter. – Brian Bi May 21 '22 at 02:19
  • @BrianBi *The control block may also be concurrently accessed but it doesn't matter. * You mean the said control block should have independent synchronisation, which is independent from the object `shared_point`. I mean the synchronisation of the control block depend on the control block itself other than the instances of the `shared_ptr`. Am I right? – John May 21 '22 at 02:46
  • @John I'm not sure I understand your question. The point is that if you only access the control block by using the `shared_ptr` API properly, then accesses to the control block will never race. This means the implementer of `shared_ptr` needs to ensure this property holds. Typically this is done by having atomic reference counts in the control block. – Brian Bi May 21 '22 at 15:43