This is a follow up question to this one.
I want to figure exactly the meaning of instruction ordering, and how it is affected by the std::memory_order_acquire
, std::memory_order_release
etc...
In the question I linked there's some detail already provided, but I felt like the provided answer isn't really about the order (which was more what was I looking for) but rather motivating a bit why this is necessary etc.
I'll quote the same example which I'll use as reference
#include <thread>
#include <atomic>
#include <cassert>
#include <string>
std::atomic<std::string*> ptr;
int data;
void producer()
{
std::string* p = new std::string("Hello");
data = 42;
ptr.store(p, std::memory_order_release);
}
void consumer()
{
std::string* p2;
while (!(p2 = ptr.load(std::memory_order_acquire)))
;
assert(*p2 == "Hello"); // never fires
assert(data == 42); // never fires
}
int main()
{
std::thread t1(producer);
std::thread t2(consumer);
t1.join(); t2.join();
}
In a nutshell I want to figure what exactly happens with the instruction order at both line
ptr.store(p, std::memory_order_release);
and
while (!(p2 = ptr.load(std::memory_order_acquire)))
Focusing on the first according to the documentation
... no reads or writes in the current thread can be reordered after this store ...
I've been watching few talks to understand this ordering issue, I understand why it is important now. The thing I cannot quite figure yet how the compiler translates the order specification, I think also the example given by the documentation isn't particularly useful as well because after the store operation in the thread running producer
there's no other instruction, hence nothing would be re-ordered anyway. However is also possible I'm missunderstanding, is it possible they mean that the equivalent assembly of
std::string* p = new std::string("Hello");
data = 42;
ptr.store(p, std::memory_order_release);
will be such that the first two lines translated will never be moved after the atomic store? Likewise in the thread running producer is it possible that none of the asserts (or the equivalent assembly) will ever be moved before the atomic load? Suppose I had a third instruction after the store what would happen to those instruction instead which would be already after the atomic load?
I've also tried to compile such code to save the intermediate assembly code with the -S
flag, but it's quite large and I can't really figure.
Again, to clarify, this question is about how the ordering, is not about why these mechanism are useful or necessary.
I know that when it comes to memory orderings, people usually try to argue if and how operations can be reorder, but in my opinion this is the wrong approach! The C++ standard does not state how instructions can be reordered, but instead defines the happens-before relation, which itself is based on the sequenced-before, synchronize-with and inter-thread-happens-before relations.
An acquire-load that reads the value from a store-release sychronizes-with that acquire load, therefore establishing a happens-before relation. Due to the transitivity of the happens-before relation, operations that are "sequenced-before" the store-release, also "happen-before" the acquire-load. Any arguments about the correctness of an implementation using atomics should always rely on the happens-before relation. If and how instructions can be reordered is merely a result of applying the rules for the happens-before relation.
For a more detailed explanation of the C++ memory model you can take a look at Memory Models for C/C++ Programmers.