I have two threads, thread A and thread B, and a global object stored in a unique_ptr
. Thread A initializes the object once, and thread B is designed to access the object only after the initialization is done. Here is my code:
std::unique_ptr<Object> object;
std::atomic<bool> isInitialized = false;
void createObject()
{
object = std::make_unique<Object>();
isInitialized = true;
}
Object& getObject()
{
if (!isInitialized)
assert(false);
return *object;
}
I'm still learning about the basics of the C++ memory model so is this solution correct to begin with? If so, am I correct that the atomic variable is necessary and it needs to be present in both functions even though thread A creates the object before thread B tries to access it since otherwise the modification of object
may not be visible to thread B?
Your code as it stands is fine. If getObject
loads the value true
from isInitialized
, then it synchronizes with the store of true
in createObject
, because the load is acquire and the store is release (in fact, here they are both sequentially consistent, which is even stronger than necessary). This implies that the call of make_unique
and the write to object
happens-before the read, so there is no data race, and the read is guaranteed to observe the new value in object
and all the effects of make_unique
. And if getObject
loads the value false
, then (assuming that NDEBUG
is not defined) it does not read from object
at all, so there is no problem.
If, as suggested in comments, you were to replace std::atomic<bool> isInitialized;
with simply bool isInitialized;
, that would be bad. In fact, we can forget about anything to do with object
; the mere fact that the non-atomic isInitialized
itself is being concurrently read and written in two different threads, is a data race all by itself, causing the program to exhibit undefined behavior. But in particular, it wouldn't ensure synchronization, and for instance the operations on object
could be reordered with those on isInitialized
.
So in fact, if isInitialized
were not atomic, your code would have at least three separate data races: one on isInitialized
itself, one on the pointer object
, and a third on the memory that object
points to. Each of these is written by createObject
and read by getObject
, with no synchronization whatsoever between them.