63

I've read that

"Multiple threads can simultaneously read and write different shared_ptr objects, even when the objects are copies that share ownership." (MSDN: Thread Safety in the Standard C++ Library)

Does that mean that changing shared_ptr object is safe ?
For an instance, is the next code considered safe:

shared_ptr<myClass> global = make_shared<myClass>();
...

//In thread 1
shared_ptr<myClass> private = global;
...

//In thread 2
global = make_shared<myClass>();
...

Can I be sure in that case that thread 1 private will have the original value of global or the new value which thread 2 assigned but either way it will have a valid shared_ptr to myClass?

==EDIT==
Just to explain my motivation. I want to have a shared pointer to hold my configuration and I have a thread pool to handle requests.
so global is the global configuration.
thread 1 is taking the current configuration as it start to handle a request.
thread 2 is updating the configuration. (only apply to future requests)

If it's work, I can update the configuration that way without breaking it in the middle of a request handling.

Roee Gavirel
  • 18,955
  • 12
  • 67
  • 94
  • 10
    *Multiple threads can simultaneously read and write different shared_ptr objects*, The key word is **different** – Alok Save Jan 23 '13 at 15:11
  • 4
    @AlokSave That was my initial thinking, but then it's seems like a redundant thing to say. of course you can read and write to different objects... – Roee Gavirel Jan 23 '13 at 15:14
  • 6
    It is not "offcourse". The smart pointer is pointing to a common helper object implementing the reference counting. With a naive implementation the reference counting on the helper object could go wrong. Still, I think your question is a good one and a definitive answer is most welcome. – Suma Jan 23 '13 at 15:18
  • I have extended my company's shared_ptr implementation to support weak_ptr. And I can tell you, there is no way of making it thread safe other than by using full fledged critical sections within methods that manipulate reference counts. (the key part is the plural here) Standards shared_ptr use atomic inc/dec and cmp/xch in the release() method to check against 0 before deleting. This is not thread safe due to the 2nd ref count (the weak ref count). A weak ref could turn shared after the test has passed, and you have a dangling. boom. – v.oddou Dec 05 '13 at 00:45
  • @v.oddou, that's why the `weak_ptr::lock()` function uses compare-and-swap, so it can know that it failed if another thread decremented the count to zero. Either I'm misunderstanding exactly what you mean by "thread safe" or your comment is incorrect. – Jonathan Wakely Aug 01 '14 at 14:18
  • @JonathanWakely: I think you understood spot on. It was a while since I thought about all this, so I went to check here again: http://www.boost.org/doc/libs/1_38_0/boost/detail/sp_counted_base_gcc_x86.hpp . I have the feeling the trick to make it work is in here: `#weak + (#shared != 0)` the weak count is a bit special, and allows for a unique check before shared_count destruction in `weak_release()`. my naive thought didnt implement this trick thus atomicality couldnt be exerced in this test. – v.oddou Aug 04 '14 at 02:07
  • 2
    `private` is a keyword, it cannot be used as a variable name – M.M Mar 14 '15 at 03:36

7 Answers7

112

What you're reading isn't meaning what you think it means. First of all, try the msdn page for shared_ptr itself.

Scroll down into the "Remarks" section and you'll get to the meat of the issue. Basically, a shared_ptr<> points to a "control block" which is how it keeps track of how many shared_ptr<> objects are actually pointing to the "Real" object. So when you do this:

shared_ptr<int> ptr1 = make_shared<int>();

While there is only 1 call to allocate memory here via make_shared, there are two "logical" blocks that you should not treat the same. One is the int which stores the actual value, and the other is the control block, which stores all the shared_ptr<> "magic" that makes it work.

It is only the control block itself which is thread-safe.

I put that on its own line for emphasis. The contents of the shared_ptr are not thread-safe, nor is writing to the same shared_ptr instance. Here's something to demonstrate what I mean:

// In main()
shared_ptr<myClass> global_instance = make_shared<myClass>();
// (launch all other threads AFTER global_instance is fully constructed)

//In thread 1
shared_ptr<myClass> local_instance = global_instance;

This is fine, in fact you can do this in all threads as much as you want. And then when local_instance is destructed (by going out of scope), it is also thread-safe. Somebody can be accessing global_instance and it won't make a difference. The snippet you pulled from msdn basically means "access to the control block is thread-safe" so other shared_ptr<> instances can be created and destroyed on different threads as much as necessary.

//In thread 1
local_instance = make_shared<myClass>();

This is fine. It will affect the global_instance object, but only indirectly. The control block it points to will be decremented, but done in a thread-safe way. local_instance will no longer point to the same object (or control block) as global_instance does.

//In thread 2
global_instance = make_shared<myClass>();

This is almost certainly not fine if global_instance is accessed from any other threads (which you say you're doing). It needs a lock if you're doing this because you're writing to wherever global_instance lives, not just reading from it. So writing to an object from multiple threads is bad unless it's you have guarded it through a lock. So you can read from global_instance the object by assigning new shared_ptr<> objects from it but you can't write to it.

// In thread 3
*global_instance = 3;
int a = *global_instance;

// In thread 4
*global_instance = 7;

The value of a is undefined. It might be 7, or it might be 3, or it might be anything else as well. The thread-safety of the shared_ptr<> instances only applies to managing shared_ptr<> instances which were initialized from each other, not what they're pointing to.

To emphasize what I mean, look at this:

shared_ptr<int> global_instance = make_shared<int>(0);

void thread_fcn();

int main(int argc, char** argv)
{
    thread thread1(thread_fcn);
    thread thread2(thread_fcn);
    ...
    thread thread10(thread_fcn);

    chrono::milliseconds duration(10000);
    this_thread::sleep_for(duration);

    return;
}

void thread_fcn()
{
    // This is thread-safe and will work fine, though it's useless.  Many
    // short-lived pointers will be created and destroyed.
    for(int i = 0; i < 10000; i++)
    {
        shared_ptr<int> temp = global_instance;
    }

    // This is not thread-safe.  While all the threads are the same, the
    // "final" value of this is almost certainly NOT going to be
    // number_of_threads*10000 = 100,000.  It'll be something else.
    for(int i = 0; i < 10000; i++)
    {
        *global_instance = *global_instance + 1;
    }
}

A shared_ptr<> is a mechanism to ensure that multiple object owners ensure an object is destructed, not a mechanism to ensure multiple threads can access an object correctly. You still need a separate synchronization mechanism to use it safely in multiple threads (like std::mutex).

The best way to think about it IMO is that shared_ptr<> makes sure that multiple copies pointing to the same memory don't have synchronization issues for itself, but doesn't do anything for the object pointed to. Treat it like that.

Kevin Anderson
  • 6,850
  • 4
  • 32
  • 54
  • 6
    what you say in your first lines is false. make_shared exist for the very good side effect of being capable of nesting both the int and the ref counters class into the same memory block. (to avoid fragmentation and limit cache misses, also to avoid the slowness of calling 2 `new`). There is only one `malloc` and 2 placement news using `make_shared`. – v.oddou Dec 05 '13 at 00:48
  • 4
    You need to read a little further: Then what's really happening is TWO different sections of memory are being allocated. It's done at one time, but it's two "logical" blocks. Thinking of it as two logical blocks is important to understand what is and is not thread safe. – Zero Dec 05 '13 at 06:56
  • 6
    v.oddou is right i that there is only one memory allocation. Then again, Zero (and Kevin) are right in that, logically, there are two memory regions, and only one of them is thread safe. Still, I think it's important to point that make_shared performs only one memory allocation. – Shachar Shemesh Apr 15 '14 at 04:38
  • 5
    I said that already, though maybe not the way you wanted: "It's done at one time, but it's two "logical" blocks." – Kevin Anderson Apr 15 '14 at 14:48
  • One of your variable is called "private" :( – Gb01 Jan 13 '16 at 13:07
  • This was 3 years ago. Every time I'm brought back to this example I cringe that I did such a thing with the variable name, but I don't usually edit my posts that are that old either. Probably should be `priv_ptr` or something, and all the others following that naming scheme. – Kevin Anderson Jan 18 '16 at 16:31
  • 2
    Forget the naming, this is a great answer! – davidhigh Apr 01 '16 at 11:48
  • 1
    Due to large amounts of internal shame that mounted over the years for naming a variable `private` in such a popular answer, I have finally fixed said variable name. I also fixed `global` to `global_instance` for clarity and consistency. – Kevin Anderson Jan 09 '17 at 19:27
  • 3
    @KevinAnderson I may be reading it wrong, but in one of your first 'this is fine' snippets with `in main, in thread1` is not it unsafe to read from global while other thread is writing to it, could it be torn write/read there? Not sure construction is atomic – Oleg Bogdanov Aug 24 '17 at 16:01
  • 3
    @OlegBogdanov I think you have a valid interpretation here. My example is a bit "not as good as it could be" in that I'm implying that the `main` action happens, THEN you launch the thread, which of course may not be right. I'll edit this to make it clearer that's what I mean. The `global_instance` should be fully constructed prior to using it in another thread. – Kevin Anderson Aug 24 '17 at 18:07
33

To add to what Kevin wrote, the C++14 spec has additional support for atomic access to shared_ptr objects themselves:

20.8.2.6 shared_ptr atomic access [util.smartptr.shared.atomic]

Concurrent access to a shared_ptr object from multiple threads does not introduce a data race if the access is done exclusively via the functions in this section and the instance is passed as their first argument.

So if you do:

//In thread 1
shared_ptr<myClass> private = atomic_load(&global);
...

//In thread 2
atomic_store(&global, make_shared<myClass>());
...

it will be thread safe.


In C++17 and later, you should use

atomic<shared_ptr<myClass>> global;

and all accesses to global will be thread safe. However, that is only legal in C++17 and later.

Chris Dodd
  • 119,907
  • 13
  • 134
  • 226
  • Have edited so that the reference numbering matches C++14 (N4140). I guess you got your reference from a post-C++14 draft since the numbering had increased, but the text is the same – M.M Mar 02 '16 at 04:50
  • And you could presumably optimize by using the `_explicit` forms, right? `... = atomic_load_explicit(&global, std::memory_order_acquire);` and `atomic_store_explicit(&global, make_shared(), std::memory_order_release);`. On strongly ordered systems like x86, [these explicit forms don't even require memory barriers, where the default usage (with `seq_cst`) would.](http://stackoverflow.com/a/18512212/364696) – ShadowRanger Nov 19 '16 at 04:41
  • 6
    It is part of C++ 11 standard section 20.7.2.5. – bop Dec 26 '16 at 20:58
  • 1
    Note that in C++17 and C++20, this is now a deprecated feature, part of Annex D in [depr.util.smartptr.shared.atomic]. Annex D is normative so you can still depend on this, but it may be removed in a future C++ standard revision – Adam Rosenfield Mar 24 '22 at 17:34
  • To add, `std::atomic_*` are deprecated due to new template vesion of `std::atomic`. However as of 2023 Jan, it's not [supported by clang yet](https://en.cppreference.com/w/cpp/compiler_support#C.2B.2B20_library_features). – Louis Go Jan 17 '23 at 07:19
5

It means you will have a valid shared_ptr, and a valid reference counting.

You're describing a race condition between 2 threads that are trying to read/assign to the same variable.

Because this is undefined behavior in general (it only makes sense in the context and timing of the individual program) shared_ptr doesn't handle that.

Guy Avraham
  • 3,482
  • 3
  • 38
  • 50
Yochai Timmer
  • 48,127
  • 24
  • 147
  • 185
2

Read operations are not subject to data races among themselves, hence it is safe to share the same instance of the shared_ptr between threads as long as all threads use const methods only (this includes creating copies of it). As soon as one thread uses non-const method (as in "point it to another object") such use is no longer thread safe.

The OP example is not thread safe and would require the use of atomic load in thread 1. and atomic store in thread 2 (section 2.7.2.5 in C++11) to make it thread safe.

The key word in MSDN text is indeed different shared_ptr objects, as already stated in previous answers.

Leon
  • 89
  • 1
  • 2
2

Summary

  • Different std::shared_ptr instances can be read from and modified by multiple threads at the same time, even if these instances are copies and share ownership of the same object.

  • The same std::shared_ptr instance can be read by multiple threads simultaneously.

  • The same std::shared_ptr instance cannot be directly modified by multiple threads without additional synchronization. But can be done by means of mutex and atomics.


Basic thread safety

The standard does not say anything about the thread-safety of smart pointers, in particular, std::shared_ptr, or how they help ensure it. As @Kevin Anderson noted above, std::shared_ptr actually provides a facility for sharing ownership of an object and ensuring that it is destroyed correctly, rather than providing correct concurrent access. In fact, std::shared_ptr, like any other built-in type, are subject to the so-called basic thread-safety guarantee. Which is defined in this paper as:

The basic thread-safety guarantee would be that standard library functions are required to be reentrant, and non-mutating uses of objects of standard library types are required to not introduce data races. This has little or no impact on performance. It does actually deliver the promised safety. Thus this basic thread-safety guarantee is required of implementations.

As for the standard, there is the following wording:

[16.4.6.10/3]

A C++ standard library function shall not directly or indirectly modify objects accessible by threads other than the current thread unless the objects are accessed directly or indirectly via the function's non-const arguments, including this.

It follows from this that the following code must be considered as thread-safe:

std::shared_ptr<int> ptr = std::make_shared<int>(100);

for (auto i= 0; i<10; i++){
    std::thread([ptr]{                        
    auto local_p = ptr;  # read from ptr
    //...
    }).detach(); 
}

But we know that a std::shared_ptr is a reference counted pointer where the object pointed to is deleted when the use count goes to zero. Reference count block of the std::shared_ptr is an implementation detail for the standard library. And despite the constant operation above (read from), the implementation needs to modify the counter. This situation is described as follows:

[16.4.6.10/7]

Implementations may share their own internal objects between threads if the objects are not visible to users and are protected against data races.

And this is what Herb Sutter calls internal synchronization:

So what’s the purpose of the internal synchronization? It’s only to do necessary synchronization on the parts that the internals know are shared and that the internals own, but that the caller can’t synchronize because he doesn’t know about the sharing and shouldn’t need to because the caller doesn’t own them, the internals do. So in the internal implementation of the type, we do just enough internal synchronization to get back to the level where the caller can assume his usual duty of care and in the usual ways correctly synchronize any objects that might actually be shared.

Thus, the basic thread-safety ensures thread-safety all operations(including copy constructor and copy assignment) on different instances of std::shared_ptr without additional synchronization even if these instances are copies and share ownership of the same object.

Strong thread-safety

But consider the following situation:

std::shared_ptr<int> ptr = std::make_shared<int>(100);

for (auto i= 0; i<10; i++){
    std::thread([&ptr]{                        
    ptr = std::make_shared<int>(200);
    //...           
    }).detach(); 
}

The lambda-function binds the std::shared_ptr ptr by reference. Therefore the assignment is a race condition on the resource (ptr object itself) and the program has undefined behavior. The basic thread-safety guarantee does not work here, and we have to use the strong thread-safety guarantee. Take this definition:

The strong thread-safety guarantee would be that mutating uses of objects of standard library types are required to not introduce data races. This would have a severe negative impact on performance. Furthermore, real safety often requires locking across several member function calls, so providing per function-call locking would create an illusion of safety that did in fact not exist. For these reasons, a blanket strong thread-safety guarantee for mutating shared objects is not provided, and constraints are put on programs accordingly.

Basically, we have to synchronize access to the same std::shared_ptr instance for a non-const operation. We can do this in the following ways:

Some examples:

std::mutex:

std::shared_ptr<int> ptr = std::make_shared<int>(100);
std::mutex mt;

for (auto i= 0; i<10; i++){
    std::thread([&ptr, &mt]{  
      std::scoped_lock lock(mt);                      
      ptr = std::make_shared<int>(200);
      //...                   
      }).detach(); 
}

Atomic functions:

std::shared_ptr<int> ptr = std::make_shared<int>(100);

for (auto i= 0; i<10; i++){
  std::thread([&ptr]{      
    std::atomic_store(&ptr, std::make_shared<int>(200));                   
  }).detach(); 
}
alex_noname
  • 26,459
  • 5
  • 69
  • 86
  • You mentioned that "the assignment is a race condition on the resource and the program has undefined behavior" for 2nd code snippet, I missed that part...what's the race condition? Are you referring to the possibility that `ptr` has multiple assignments from multiple threads? – HCSF Jan 07 '22 at 13:16
  • 1
    Yes, I meant a concurrent write access to the same object `ptr` from different threads. – alex_noname Jan 07 '22 at 14:09
1

I think the so far answers to this question are misleading with regard to described scenario. I have a very similar scenario described in the question. All other threads have (need) just a read-only access to current configuration which is achieved through:

// In thread n
shared_ptr<MyConfig> sp_local = sp_global;

None of these threads are going to modify the content of the MyConfig object. Ref count for sp_global gets incremented for each execution of the line above.

Thread 1, periodically resets the sp_global to some another instance of the configuration:

// In thread 1
shared_ptr<MyConfig> sp_global = make_shared<MyConfig>(new MyConfig);

This also should be safe. It sets the ref count of sp_global back to 1, and the sp_global now points to the latest configuration, as goes with all the new local copies. So, if I am not missing anything here, this should all be totally thread safe.

#include <iostream>
#include <memory>

using namespace std;

shared_ptr<int> sp1(new int(10));

int main()
{
    cout<<"Hello World! \n";

    cout << "sp1 use count: " << sp1.use_count() << ", sp1: " << *sp1 << "\n";
    cout << "---------\n";

    shared_ptr<int> sp2 = sp1;
    shared_ptr<int>* psp3 = new shared_ptr<int>;
    *psp3 = sp1;
    cout << "sp1 use count: " << sp1.use_count() << ", sp1: " << *sp1 << "\n";
    cout << "sp2 use count: " << sp2.use_count() << ", sp2: " << *sp2 << "\n";
    cout << "sp3 use count: " << psp3->use_count() << ", sp3: " << *(*psp3) << "\n";
    cout << "---------\n";

    sp1.reset(new int(20));

    cout << "sp1 use count: " << sp1.use_count() << ", sp1: " << *sp1 << "\n";
    cout << "sp2 use count: " << sp2.use_count() << ", sp2: " << *sp2 << "\n";
    cout << "sp3 use count: " << psp3->use_count() << ", sp3: " << *(*psp3) << "\n";
    cout << "---------\n";

    delete psp3;
    cout << "sp1 use count: " << sp1.use_count() << ", sp1: " << *sp1 << "\n";
    cout << "sp2 use count: " << sp2.use_count() << ", sp2: " << *sp2 << "\n";
    cout << "---------\n";

    sp1 = nullptr;

    cout << "sp1 use count: " << sp1.use_count() << "\n";
    cout << "sp2 use count: " << sp2.use_count() << ", sp2: " << *sp2 << "\n";

    return 0;
}

and the output

Hello World!
sp1 use count: 1, sp1: 10
---------
sp1 use count: 3, sp1: 10
sp2 use count: 3, sp2: 10
sp3 use count: 3, sp3: 10
---------
sp1 use count: 1, sp1: 20
sp2 use count: 2, sp2: 10
sp3 use count: 2, sp3: 10
---------
sp1 use count: 1, sp1: 20
sp2 use count: 1, sp2: 10
---------
sp1 use count: 0
sp2 use count: 1, sp2: 10
hagh
  • 507
  • 5
  • 13
0

here is my understanding of thread safety of shared_ptr. IMO, there are three aspects when it comes to thread safety of shared_ptr.

The first one is shared_ptr itself. I would say shared_ptr itself is not thread safe, which means there is a data race when we try to access one shared_ptr object in multiple threads and one of the accesses is writing. For example, we have a data race in the following situation:

# Main Thread
shared_ptr<string> global_ptr = make_shared<string>();
string str = *global_ptr;

# Thread 1
global_ptr.reset();

The second aspect is the internal structure of shared_ptr. I would say it is thread safe. The result is there is no data race when accessing multiple shared_ptr objects and the objects point to the same managed object. For example, we don't have data race in the following situation:

# Main Thread
shared_ptr<string> global_ptr = make_shared<string>();
string str = *global_ptr;

# Thread 1
shared_ptr<string> local_ptr = global_ptr;
local_ptr.reset();

The third aspect is that the managed object in the shared_ptr might or might not be thread safe. For example, I would say there is a data race in the following situation:

# Main Thread
shared_ptr<string> global_ptr = make_shared<string>();
string str = *global_ptr;

# Thread 1
shared_ptr<string> local_ptr = global_ptr;
(*local_ptr).clear();
References

https://gcc.gnu.org/onlinedocs/libstdc++/manual/memory.html#shared_ptr.thread

https://en.cppreference.com/w/cpp/memory/shared_ptr/atomic

Lujun Weng
  • 101
  • 5