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