Search code examples
c++concurrencyshared-ptr

Is the copy constructor of `std::shared_ptr` atomic versus `reset()`?


This is a verification question, to make sure I'm getting details right—language lawyers welcome.

I want to know that I can use std::shared_ptr in the following code and don't need to rewrite it with atomic_shared_ptr. The example has been simplified, but the essence is a possible race condition within a single instance of example between (*1) the copy constructor of shared_ptr and (*2) the call to reset().

Note that a plain pointer for p won't work here. If p were to become null between the test and the call to some_predicate, you'd be indirecting a null pointer. That's the reason to use shared_ptr in the first place. I want to make sure I'm actually solving the race condition, not simply moving it elsewhere.

(It's not the point of the question, but this code might seem wrong at first glance. The behavior of T is idempotent. Once p has finished its job, it's not needed any more.)

template< class T >
class example
{
    shared_ptr< T > p ;
public:
    example()
        : p( make_shared( T() ) )
    {}
    void f()
    {
        shared_ptr< T > p_transient(p) ; // *1
        if ( p_transient && p_transient -> some_predicate() )
        {
            p.reset() ; // *2
        }
    }
};

Suppose that (*1) and (*2) execute simultaneously. I can think of two possible results of the race. (The code is correct in each of these two cases.) My question is whether these are the only cases:

  • The copy takes effect before the reset, so p_transient keeps the member instance of T alive. The deleter runs in thread *1 when f returns. (The idempotence of T comes into play here.)
  • reset takes effect before the copy, so p_transient is initialized empty. The deleter runs in thread *2 before reset returns.

I can't shake the feeling I'm getting something for nothing here, so I decided to write up the question. Anything I'm missing?


P.S. Here's what I was missing. shared_ptr isn't special. Somehow I thought it would be, perhaps because I've implemented smart pointers (too many) times before. Shared pointers, particularly when there are also weak pointers, pretty much require mutex protection for their (hidden) shared state. I figured that protection must have encompassed the entire object, but it doesn't.

Thanks to the responders for references to the standard. The general rule that data races result in undefined behavior is 1.10/27 "Multi-threaded executions and data races [intro.multithread]". In particular, it means that postconditions may be violated in such a situation.


Solution

  • What you are looking at is called a data race. Any time one thread may write to some data and another thread may read or write that data, it is known as a data race.

    Data races are undefined behavior. This means there is no limit to what could occur. I swear by the blog entry Benign race cases: what could possibly go wrong? for these sorts of things. He goes through a list of things which could go wrong.

    One example is that if you write to a memory location, the compiler is actually allowed to use this memory space to hold spilled registers. It doesn't happen often, but it can happen. The blog mentioned above shows an extreme example where a data race in this form launches a nuclear missile unintentionally! (Here's hoping the real nuclear missile launch computers are a bit more robust!)

    If you want to have two threads interacting with a piece of data, you must prevent data races. This is usually done with mutexes or atomics.