BCS BCS - 27 days ago 13
C++ Question

Are constructors thread safe in C++ and/or C++11?

Derived from this question and related to this question:

If I construct an object in one thread and then convey a reference/pointer to it to another thread, is it thread un-safe for that other thread to access the object without explicit locking/memory-barriers?

// thread 1
Obj obj;

anyLeagalTransferDevice.Send(&obj);
while(1); // never let obj go out of scope

// thread 2
anyLeagalTransferDevice.Get()->SomeFn();


Alternatively: is there any legal way to convey data between threads that doesn't enforce memory ordering with regards to everything else the thread has touched? From a hardware standpoint I don't see any reason it shouldn't be possible.

To clarify; the question is with regards to cache coherency, memory ordering and whatnot. Can Thread 2 get and use the pointer before Thread 2's view of memory includes the writes involved in constructing
obj
? To miss-quote Alexandrescu(?) "Could a malicious CPU designer and compiler writer collude to build a standard conforming system that make that break?"

Answer

Reasoning about thread-safety can be difficult, and I am no expert on the C++11 memory model. Fortunately, however, your example is very simple. I rewrite the example, because the constructor is irrelevant.

Simplified Example

Question: Is the following code correct? Or can the execution result in undefined behavior?

// Legal transfer of pointer to int without data race.
// The receive function blocks until send is called.
void send(int*);
int* receive();

// --- thread A ---
/* A1 */   int* pointer = receive();
/* A2 */   int answer = *pointer;

// --- thread B ---
           int answer;
/* B1 */   answer = 42;
/* B2 */   send(&answer);
           // wait forever

Answer: There may be a data race on the memory location of answer, and thus the execution results in undefined behavior. See below for details.


Implementation of Data Transfer

Of course, the answer depends on the possible and legal implementations of the functions send and receive. I use the following data-race-free implementation. Note that only a single atomic variable is used, and all memory operations use std::memory_order_relaxed. Basically this means, that these functions do not restrict memory re-orderings.

std::atomic<int*> transfer{nullptr};

void send(int* pointer) {
    transfer.store(pointer, std::memory_order_relaxed);
}

int* receive() {
    while (transfer.load(std::memory_order_relaxed) == nullptr) { }
    return transfer.load(std::memory_order_relaxed);
}

Order of Memory Operations

On multicore systems, a thread can see memory changes in a different order as what other threads see. In addition, both compilers and CPUs may reorder memory operations within a single thread for efficiency - and they do this all the time. Atomic operations with std::memory_order_relaxed do not participate in any synchronization and do not impose any ordering.

In the above example, the compiler is allowed to reorder the operations of thread B, and execute B2 before B1, because the reordering has no effect on the thread itself.

// --- valid execution of operations in thread B ---
           int answer;
/* B2 */   send(&answer);
/* B1 */   answer = 42;
           // wait forever

Data Race

C++11 defines a data race as follows (N3290 C++11 Draft): "The execution of a program contains a data race if it contains two conflicting actions in different threads, at least one of which is not atomic, and neither happens before the other. Any such data race results in undefined behavior." And the term happens before is defined earlier in the same document.

In the above example, B1 and A2 are conflicting and non-atomic operations, and neither happens before the other. This is obvious, because I have shown in the previous section, that both can happen at the same time.

That's the only thing that matters in C++11. In contrast, the Java Memory Model also tries to define the behavior if there are data races, and it took them almost a decade to come up with a reasonable specification. C++11 didn't make the same mistake.


Further Information

I'm a bit surprised that these basics are not well known. The definitive source of information is the section Multi-threaded executions and data races in the C++11 standard. However, the specification is difficult to understand.

A good starting point are Hans Boehm's talks - e.g. available as online videos:

There are also a lot of other good resources, I have mentioned elsewhere, e.g.: