Where do I set memory barriers so that conditional loops observe multithread value changes?

Viewed 211

Assuming a thread running indefinitely until it's member variable stop is set. Like

while (!stop) {
  // do something
}

I want to prevent it turning into this

if (!stop) {
  while (true) {
  
  }
}

So where would I set memory barriers / fences so that this optimization can't be performed?
Also, would such a fence already be enough to ensure a change to true from another thread is visible in the next iteration?

3 Answers

So where would I set memory barriers / fences so that this optimization can't be performed?

A memory barrier/fence restricts the memory ordering, which prevents the compiler from reordering two or more memory accesses in the same thread. However, memory ordering is not the issue here, as you are only talking about a single memory access per thread.

Instead, you are talking about two memory operations on the same variable from two different threads. Therefore, this is a matter of thread synchronization, not memory ordering.

According to §6.9.2.2 ¶21 of the ISO C++20 standard, if two threads access the same variable without a synchronization operation (e.g. mutex) in between, and if these memory accesses are not

  1. both atomic, or
  2. both read-only,

then this results in undefined behavior.

Therefore, in the case of your while loop

while (!stop) {
  // do something
}

assuming that stop is a non-atomic data type, the compiler is allowed to assume that the variable stop will never be modified by another thread while the loop is running (unless you happen to have a synchronization operation inside the loop), because doing so would result in undefined behavior (which means that the compiler would be allowed to do anything).

For this reason, a good optimizing compiler will effectively change the code to the following:

if (!stop) {
  while (true) {  
    // do something
  }
}

However, if you change the variable stop to an atomic data type, such as std::atomic_flag or std::atomic<bool>, then it is not undefined behavior for several threads to access this variable at the same time. In that case, the compiler will not be allowed to assume that stop will not be modified by another thread, so that it will not be allowed to perform the optimization mentioned above.

It's a short one and has been told in comments, but the answer is:

std::atomic<bool> stop;

Let the compiler deal with fences.

As it's already been answered, a simple:

std::atomic<bool> stop;

suffices, as operations on atomic variables are ordered in a sequentially-consistent manner.

However, release semantics could be used on stores and acquire semantics on loads to a similar effect:

bool side_effects = false;
std::atomic<bool> stop = false;

std::thread do_stop(
    [&]
    {
        side_effects = true;
        stop.store(true, std::memory_order_release);
    });

while(!stop.load(std::memory_order_acquire))
{
}

// Any side effect ordered before the release store is necessarily
// visible after such store is sensed by the acquire load.
assert((side_effects));

do_stop.join();

See it live.

Related