A confusing example in cppreference is
#include <thread>
#include <atomic>
#include <cassert>
std::atomic<bool> x = {false};
std::atomic<bool> y = {false};
std::atomic<int> z = {0};
void write_x()
{
x.store(true, std::memory_order_seq_cst);
}
void write_y()
{
y.store(true, std::memory_order_seq_cst);
}
void read_x_then_y()
{
while (!x.load(std::memory_order_seq_cst))
;
if (y.load(std::memory_order_seq_cst)) {
++z;
}
}
void read_y_then_x()
{
while (!y.load(std::memory_order_seq_cst))
;
if (x.load(std::memory_order_seq_cst)) {
++z;
}
}
int main()
{
std::thread a(write_x);
std::thread b(write_y);
std::thread c(read_x_then_y);
std::thread d(read_y_then_x);
a.join(); b.join(); c.join(); d.join();
assert(z.load() != 0); // will never happen
}
The annotation says that only the std::memory_order_seq_cst
can guarantee the assert not be fired. However, Isn't std::memory_order_acquire
for load
and std::memory_order_release
for store
for the same atomic variable is sufficient here? IMO, all memory writes before M.store(x, std::memory_order_release)
in one thread are visible to the reads after M.load(std::memory_order_acquire)
, for example
int x = 0, y = 0;
std::atomic<int> atomic_i32 = {0};
// Thread 1:
x = 2;
y = 3;
atomic_i32.store(1,std::memory_order_release);
// Thread 2:
while(atomic_i32.load(std::memory_order_acquire)!=1);
assert(x==2); // no fired
assert(y==3); // no fired
This example can demonstrate the effect of the pairing use of std::memory_order_acquire
and std::memory_order_release
. So, why doesn't this kind of memory order guarantee the first example? The modified version might be
void write_x()
{
x.store(true, std::memory_order_release);
}
void write_y()
{
y.store(true, std::memory_order_release);
}
void read_x_then_y()
{
while (!x.load(std::memory_order_acquire)) // #1
;
if (y.load(std::memory_order_acquire)) {
++z;
}
}
void read_y_then_x()
{
while (!y.load(std::memory_order_acquire)) // #2
;
if (x.load(std::memory_order_acquire)) {
++z;
}
}
int main()
{
std::thread a(write_x);
std::thread b(write_y);
std::thread c(read_x_then_y);
std::thread d(read_y_then_x);
a.join(); b.join(); c.join(); d.join();
assert(z.load() != 0); // fired?
}
Doesn't std::memory_order_acquire
also prevent the code from being moved to part before the load
? Which makes z
at least greater than zero?