0

I am curious to know if anybody knows why a simple while loop in c++ will not terminate without either a "usleep" or "printf"?

I have a boolean value that has its value changed externally, which its value is designed to terminate a while loop. I have also tried this and it fails:

if (!run) { break; }

It works perfectly fine with one of "usleep" or "printf" in the loop.

I have a gut feeling it is something to do with an interrupt, but not certain why.

while (run)
{
     // Do something

     // usleep OR printf
}

While i can easily do "usleep(0)" and it works, I am quite curious as to why this happens. My system is Ubuntu 16.04, running C++11 (GCC/G++ 5.4).

Thanks, CaptainJL

aptainJL
  • 3
  • 1
  • 2
    At a guess, you're doing something like reading this on one thread while writing on another without synchronization. It's hard to say without a [mcve]. – chris Oct 31 '18 at 03:28

2 Answers2

2

The official answer is: If you're sharing a (non-atomic) boolean variable across threads and not serializing access to that variable (e.g. with a mutex), then you've invoked undefined behavior, and the program is free to do whatever it likes (work as expected, work some other way, crash, steal cash from your wallet, etc).

The more practical answer is: On a modern multi-core machine, each core has its own registers and a separate L1 cache, and so if a thread running on core 1 sets a particular memory location, then another thread running on core 2 may not "see" that change unless the compiler has taken specific steps to make sure that the change is propagated across cores. Furthermore, unless you (the programmer) have taken explicit measures to let the compiler know that a particular variable is going to be used by multiple threads, the compiler's optimizer may assume that that variable cannot be changed outside of a given thread's flow of execution, and may therefore remove your test of the variable's state entirely (because after all, if the compiler has "proven" that the variable's value cannot be changed, why waste CPU cycles checking its state?).

What's likely happening in your case is that the calls to printf() or usleep() have side effects (e.g. a usermode->kernel->usermode switch) that include flushing the core's cache, such that the second thread (at least eventually) "sees" the change made by the first thread -- while without those calls, there is no reason to synchronize caches and thus the update is never "seen".

The practical advice: If you're going to share a variable across threads, be sure to either use a std::atomic<bool> instead of a plain-old bool, or serialize/protect all accesses (reads and writes) to that variable inside a mutex or critical section. Otherwise you're likely to run into lots "interesting" behavior of this sort.

Jeremy Friesner
  • 70,199
  • 15
  • 131
  • 234
  • Minor Nit: Just using a Mutex won't necessarily be enough on some platforms, volatile is also required. That said, std::atomic<> is the proper, modern, method. – Rich Nov 01 '18 at 03:29
  • @Rich in my understanding a Mutex is always sufficient (if it is not, then the Mutex implementation is buggy). Volatile, OTOH, was a keyword that was introduced before the widespread use of multithreading, and as such does not provide the necessary behavioral guarantees to be reliably usable as a thread-safety mechanism. (volatile's focus was dealing with things like memory-mapped devices and DMA hardware that can directly alter memory without the CPU's involvement) – Jeremy Friesner Nov 01 '18 at 04:34
  • A mutex is sufficient to avoid temporal collisions. It will not guarantee cache flushing or any memory synchronization - it can't it's a completely unrelated bit of code. And caching effects would be from the side effect of context switching and the compiler would still be free to optimize out writes to memory. – Rich Nov 01 '18 at 05:03
  • Can you point to some documentation that backs up that claim? My understanding is that any mutex implementation is required to do the necessary cache manipulations to make local changes visible to other cpus. – Jeremy Friesner Nov 01 '18 at 14:10
  • Unless the mutex implementation is aware of the entire thread context, including all globals, register variables, and shared memory, it cannot do what you are asserting. I've not seen every mutex implementation in the universe, but the ones I have are all about synchronization of code execution - you will see memory barrier instructions in some libraries, and that should be sufficient, it's quite platform specific and doesn't account for data cached away in registers. This is why std::atomic is a nice feature to have. – Rich Nov 01 '18 at 18:15
  • It doesn't need to be aware of the entire thread context; it just needs to include a memory barrier and trigger a flush of the L1 cache. pthreads, at least, guarantees that it will do so (see link); I believe all other common threading-library implementations do as well, since if they did not, they would not be able to provide the behavioral guarantees they promised to provide, and would be considered buggy. http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap04.html#tag_04_11 – Jeremy Friesner Nov 01 '18 at 18:25
  • I guess I've just been bitten by old implementations on old CPU's with buggy compilers far too many times to trust such things without verification. I've spent more time chasing that sort of pain to believe that a Mutex actually solves _everything_. Regardless, thanks for the discussion and the links. – Rich Nov 01 '18 at 18:40
  • Ah thanks. I did not know of the atomic parts, it solved the problem easily. Did not require volatile. – aptainJL Nov 15 '18 at 02:21
1

First guess: the "run" variable isn't declared as volatile.

Rich
  • 640
  • 5
  • 12
  • Depending on what the OP is actually doing, `volatile` might not get rid of the UB. – chris Oct 31 '18 at 03:48
  • "almost never" That's just silly. Prior to C++11 it was mandatory. – Rich Nov 01 '18 at 02:10
  • @Rich have a read here: https://stackoverflow.com/questions/2484980/why-is-volatile-not-considered-useful-in-multithreaded-c-or-c-programming – Jeremy Friesner Nov 01 '18 at 04:37
  • Clean, but still doesn't contradict anything I've said. Yes, std::atomic is "better" and a more general solution but that doesn't invalidate the volatile keyword's usefulness. – Rich Nov 01 '18 at 05:00