The example above can also be implemented with fences and relaxed atomic operations:
int x, y;
std::atomic<bool> ready{false};
void init()
{
x = 2;
y = 3;
atomic_thread_fence(std::memory_order_release);
ready.store(true, std::memory_order_relaxed);
}
void use()
{
if (ready.load(std::memory_order_relaxed))
{
atomic_thread_fence(std::memory_order_acquire);
std::cout << x + y;
}
}
If the atomic load operation sees the value written by the atomic store then the store happens before the load, and so do the fences: the release fence happens before the acquire fence making the writes to x
and y
that precede the release fence to become visible to the std::cout
statement that follows the acquire fence.
A fence might be beneficial if it can reduce the overall number of acquire, release or other synchronization operations. For example:
void block_and_use()
{
while (!ready.load(std::memory_order_relaxed))
;
atomic_thread_fence(std::memory_order_acquire);
std::cout << x + y;
}
The block_and_use()
function spins until the ready
flag is set with the help of relaxed atomic load. Then a single acquire fence is used to provide the needed memory ordering.