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?"
Static constructors are always thread safe. The runtime guarantees that a static constructor is only called once. So even if a type is called by multiple threads at the same time, the static constructor is always executed one time.
In conclusion, const does mean thread-safe from the Standard Library point of view. It is important to note that this is merely a contract and it won't be enforced by the compiler, if you break it you get undefined behavior and you are on your own.
A constructor is a special type of member function that is called automatically when an object is created. In C++, a constructor has the same name as that of the class and it does not have a return type. For example, class Wall { public: // create a constructor Wall() { // code } };
The C++ new and delete operators are thread safe, but this means that a thread may have to wait for a lock on these operations. Once memory is obtained for a thread, the thread_alloc memory allocator keeps that memory available for the thread so that it can be re-used without waiting for a lock.
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.
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.
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); }
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
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.
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.:
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With