c++multithreadingstdatomic

How to synchronize a global object between two threads in C++?


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?


Solution

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