Reputation: 4575
std::shared_ptr specification guarentees that only one thread will invoke delete on the internal pointer. This answer has a really nice explanation about the required memory ordering on the shared_ptr refrence count manipulation in order to guarentee that the delete will be invoked on a synchronized memory.
What I don't understand is the following:
I am looking at MVCC implementation of shared_ptr copy constructor. and I think I can identify at least one race condition.
template<class _Ty2>
void _Copy_construct_from(const shared_ptr<_Ty2>& _Other)
{ // implement shared_ptr's (converting) copy ctor
if (_Other._Rep)
{
_Other._Rep->_Incref();
}
_Ptr = _Other._Ptr;
_Rep = _Other._Rep;
}
The implementation checks that the control block is valid, then incerement its reference count, and copy assigns the internal fields.
Assuming _Other
is owned by a different thread then the one calling the copy constructor. If between the lines if (_Other._Rep)
and _Other._Rep->_Incref();
this thread calls the destructor that happens to delete the control block and the pointer, then _Other._Rep->_Incref()
will dereference a deleted pointer.
Here is a code that illustrates the corner case I am talking about. I will tweak share_ptr copy constructor implementation to simulate a context switch:
template<class _Ty2>
void _Copy_construct_from(const shared_ptr<_Ty2>& _Other)
{ // implement shared_ptr's (converting) copy ctor
if (_Other._Rep)
{
// now lets put here a really long loop or sleep to simulate a context switch
int count = 0;
for (int i = 0; i < 99999999; ++i)
{
for (int j = 0; j < 99999999; ++j)
{
count++;
}
}
// by the time we get here, the owning thread may already destroy the shared_ptr that was passed to this constructor
_Other._Rep->_Incref();
}
_Ptr = _Other._Ptr;
_Rep = _Other._Rep;
}
And here is a code that will probably show the problem:
int main()
{
{
std::shared_ptr<int> sh1 = std::make_shared<int>(123);
auto lambda = [&]()
{
auto sh2 = sh1;
std::cout << sh2.use_count(); // this prints garbage, -572662306 in my case
};
std::thread t1(lambda);
t1.detach();
// main thread destroys the shared_ptr
// background thread probably did not yet finished executing the copy constructor
}
Sleep(10000);
}
Upvotes: 2
Views: 2062
Reputation: 473212
Manipulation of the state shared by shared_ptr
objects is thread-safe; shared_ptr
itself is not thread-safe. You cannot manipulate the same shared_ptr
object from different threads at the same time; attempting to do so is a data race and thus UB.
So your code would be fine if lambda
copied the pointer before being shipped off to another thread.
It should also be noted that your specific example could never work, no matter how shared_ptr
was written. The type could be atomic<int>
and it would still be just as broken. You gave the lambda a reference to an object that may not exist before the lambda gets to execute the copy operation.
No amount of internal thread-safety can save you there. Passing a reference to a stack variable to another thread should always be looked on as a code smell.
Upvotes: 3
Reputation: 595377
When shared_ptr
is used properly, what your describe can never happen.
The shared_ptr
that is being copied from has incremented the refcount before being passed to the copy constructor, and it can't be destructed until after the copy constructor has exited since it is a local parameter of the constructor.
Thus, another thread will not destroy the object that is being shared. The refcount of _Other.Rep
will always be at least 1 upon entry into the copy constructor if _Other.Rep
is not null.
UPDATE: your use case is faulty. The lambda captures a reference to the main thread's shared_ptr
instance but the thread does not make a copy of that shared_ptr
until after it has already gone out of scope and been destroyed by main
. Your thread has a dangling reference causing your code to have undefined behavior. That is not a fault of the shared_ptr
implementation. Your lambda needs to capture the shared_ptr
by value instead of by reference instead, so its refcount is incremented immediately, before the thread is created, not when the thread starts running.
Upvotes: 4