std::shared_ptr thread safety explained
As others have pointed out, you've got it figured out correctly regarding your original 3 questions.
But the ending part of your edit
Calling reset() in thread IV will delete previous instance of A class created in first thread and replace it with new instance? Moreover after calling reset() in IV thread other threads will see only newly created object?
is incorrect. Only d
will point to the new A(10)
, and a
, b
, and c
will continue to point to the original A(1)
. This can be seen clearly in the following short example.
#include <memory>
#include <iostream>
using namespace std;
struct A
{
int a;
A(int a) : a(a) {}
};
int main(int argc, char **argv)
{
shared_ptr<A> a(new A(1));
shared_ptr<A> b(a), c(a), d(a);
cout << "a: " << a->a << "\tb: " << b->a
<< "\tc: " << c->a << "\td: " << d->a << endl;
d.reset(new A(10));
cout << "a: " << a->a << "\tb: " << b->a
<< "\tc: " << c->a << "\td: " << d->a << endl;
return 0;
}
(Clearly, I didn't bother with any threading: that doesn't factor into the shared_ptr::reset()
behavior.)
The output of this code is
a: 1 b: 1 c: 1 d: 1
a: 1 b: 1 c: 1 d: 10
std::shared_ptr thread safety
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.
Thread safety with std::shared_ptr
std::shared_ptr
instances are not thread safe. Multiple instances all pointing to the same object can be modified from multiple threads but a single instance is not thread safe. See https://en.cppreference.com/w/cpp/memory/shared_ptr:
All member functions (including copy constructor and copy assignment) can be called by multiple threads on different instances of shared_ptr without additional synchronization even if these instances are copies and share ownership of the same object. If multiple threads of execution access the same shared_ptr without synchronization and any of those accesses uses a non-const member function of shared_ptr then a data race will occur; the shared_ptr overloads of atomic functions can be used to prevent the data race.
You therefore either need to lock your mutex in your get_obj
method or use std::atomic_load
and std::atomic_store
in your start
and stop
methods
What mechanism ensures that std::shared_ptr control block is thread-safe?
There isn't really much machinery required for this. For a rough sketch (not including all the requirements/features of the standard std::shared_ptr
):
You only need to make sure that the reference counter is atomic, that it is incremented/decremented atomically and accessed with acquire/release semantics (actually some of the accesses can even be relaxed).
Then when the last instance of a shared pointer for a given control block is destroyed and it decremented the reference count to zero (this needs to be checked atomically with the decrement using e.g. std::atomic::fetch_add
's return value), the destructor knows that there is no other thread holding a reference to the control block anymore and it can simply destroy the managed object and clean up the control block.
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.)
Thread Safety of Shared Pointers' Control Block
In a multithreaded environment, use_count()
is approximate. From cppreference :
In multithreaded environment, the value returned by use_count is approximate (typical implementations use a memory_order_relaxed load)
The control block for shared_ptr
is otherwise thread-safe :
All member functions (including copy constructor and copy assignment) can be called by multiple threads on different instances of shared_ptr without additional synchronization even if these instances are copies and share ownership of the same object.
Seeing an out-of-date use_count()
is not an indication that the control-block was corrupted by a race condition.
Note that this does not extend to modifying the pointed object. It does not provide synchronization for the pointed object. Only the state of the shared_ptr
and the control block are protected.
Is std::move of shared_ptr thread safe?
You have a misconception about what std::move
does. In fact std::move
does nothing. It's just a compile time mechanism with the meaning: I no longer need this named value.
This then causes the compiler to use the value in different ways, like call a move constructor/assignment instead of copy constructor/assignment. But the std::move
itself generates no code so nothing to be affected by threads.
The real question you should be asking is whether the move constructor of shared_ptr
is thread safe and the answer is: No.
But the point of the shared_ptr
is not to share the shared_ptr
but to share what it points too. Do not pass a shared_ptr
by reference to threads, instead pass it by value so each thread gets it own shared_ptr
. Then it is free to move that around at will without problems.
The shared_ptr
protects the lifetime of the thing it points to. The reference count the shared_ptr
uses is thread safe but nothing else. It only manages the lifetime of the pointed to objects in a thread safe way so it doesn't get destroyed as long as any shared_ptr
has hold of the object.
Thread-safety of reference count in std::shared_ptr
Question 1:
The implementation linked to is not thread-safe at all. You are correct that the shared reference counter should be atomic, not pointers to it. std::atomic<int*>
here makes no sense.
Note that just changing std::atomic<int*>
to std::atomic<int>*
won't be enough to fix this either. For example the destructor is decrementing the reference count and checking it against 0
non-atomically. So another thread could get in between these two operations and then they will both think that they should delete the object causing undefined behavior.
As mentioned by @fabian in the comments, it is also far from a correct non-thread-safe shared pointer implementation. For example with the test case
{
Shared_ptr<int> a(new int);
Shared_ptr<int> b(new int);
b = a;
}
it will leak the second allocation. So it doesn't even do the basics correctly.
Even more, in the simple test case
{
Shared_ptr<int> a(new int);
}
it leaks the allocated memory for the reference counter (which it always leaks).
Question 2:
There is no reason to have a null pointer check there except to avoid printing the message. In fact, if we want to adhere to the standard's specification of std::default_delete
for default_deleter
, then at best it is wrong to check for nullptr
, since that is specified to call delete
unconditionally.
But the only possible edge case where this could matter is if a custom operator delete
would be called that causes some side effect for a null pointer argument. However, it is anyway unspecified whether delete
will call operator delete
if passed a null pointer, so that's not practically relevant either.
Related Topics
C++ Compare Char Array with String
Is It a Good Idea to Return " Const Char * " from a Function
Use C++ with Cocoa Instead of Objective-C
Simple Object Detection Using Opencv and MAChine Learning
How to Use Boost Bind with a Member Function
C++ Sort Keeping Track of Indices
Linux Allocator Does Not Release Small Chunks of Memory
What's the Behavior of an Uninitialized Variable Used as Its Own Initializer
Difference Between Pointer to a Reference and Reference to a Pointer
Std::Fstream Buffering VS Manual Buffering (Why 10X Gain with Manual Buffering)
How to Compile/Link Boost with Clang++/Libc++
Function Signature-Like Expressions as C++ Template Arguments
What Are the Signs of Crosses Initialization
What Does the Symbol \0 Mean in a String-Literal
How to Create Timer Events Using C++ 11
Type Erasing Type Erasure, 'Any' Questions