I have question related with following code
#include <atomic>
#include <thread>
#include <assert.h>
std::atomic<bool> x, y;
std::atomic<int> z;
void write_x_then_y()
{
x.store(true, std::memory_order_relaxed);
y.store(true, std::memory_order_relaxed);
}
void read_y_then_x()
{
while (!y.load(std::memory_order_acquire));
if (x.load(std::memory_order_acquire))
++z;
}
int main()
{
x = false;
y = false;
z = 0;
std::thread a(write_x_then_y);
std::thread b(read_y_then_x);
a.join();
b.join();
assert(z.load() != 0);
}
Can i be sure about assert(z.load() != 0) is always be false? I think x.store and y.store is not reordered in data provider thread (that is true?). For this reason i think if thread which loading values stored by x and y uses memory_order_acquire, it was get actual values for x and y from cache of core which performs storing operators.
I think the assert may fail. The
std::memory_order_relaxed
allows the compiler to reorder the stores insidewrite_x_then_y
. (For example if it thinks it would be faster for any reason.) So it may writey
beforex
. The wholeread_y_then_x
may happen between those two writes and so it will observey
beingtrue
,x
beingfalse
and it won't incrementz
.Although michalsrb already answered it, I'm adding my answer because he started with "I think" ;).
The C++ memory model allows the assert to fail.
Here are some notes:
#include <cassert>
; standard headers don't end on .h.atomic<bool>
andatomic<int>
are extremely likely to be (lock-free and) POD; by defining them in global namespace they will be initialized with an image of all zeroes; aka, they will have the valuefalse
and0
respectively even before reaching main. However, C++ has a special macro to initialize atomics: ATOMIC_VAR_INIT. Using that guarantees correct initialization and when the variable has static storage duration, this initialization is constant initialization. (When initialized correctly) The three assignments at the beginning of main have no effect. On the other hand, in general (e.g. when the atomic variable is on the stack) there is a difference between an explicit initialization and an assignment: the initial initialization is non-atomic. The assignment is turned into a store with memory order seq_cst. A better style here therefore would be to useATOMIC_VAR_INIT
and leave out the initialization at the start ofmain
:Note that if for some reason one needs to (re)initialize an atomic at run time (and not where it is being defined), you should use atomic_init.
std::memory_order_acquire
only causes a synchronization when reading the value written with astore memory_order_release
(which includesmemory_order_seq_cst
that is both release and acquire). But since you don't have astore
withmemory_order_release
in a different thread, there is certainly not going to be any synchronization going to happen. The initialization inmain
was seq_cst, but that was done before thread b was even created, so there is already a synchronization there (nl. Also-Synchronizes-With, which is much like the inter-thread Sequenced-Before relationship). Hence, usingstd::memory_order_relaxed
instead ofstd::memory_order_acquire
will do the same thing and using explicitly memory_order_acquire seems a bit strange here.So then, because there is no synchronization between threads a and b, there is no synchronization between the order in which both threads see changes to x and y and thread b can see y become true before it sees x to become true.
Do not try to understand this with compiler reordering, or hardware pipelines or anything; this is the abstract C++ memory model "computer" which is independent of whatever implementation (compiler) or hardware you might be using. It is simply a fact that this reordering is allowed to take place. With that in mind, thread b can finished and be joined leaving z still at its value of 0.
It might be instructive to see what would happen if you change your program into:
Thread b still will hang on the
while
until it reads the valuetrue
for y. So, it reads the value written by thread 1 with a storememory_order_release
! Note that the load of y was still done withmemory_order_acquire
. Now a synchronization takes place: everything that was written to any memory location before the store/release that we read from will be visible in the thread that did the read/acquire after that read. In other words, now thex.store(true, std::memory_order_relaxed);
of thread a will be visible in thread b when it performs the load of x; and the assert will never fail.