1

Based on the info of the output, can anyone explains the code below?

How can all of (a==1 && a==2 && a==3) be true?

#include <iostream>
#include <thread>

int a = 0;

int main()
{
  std::thread runThread([]() { while (true) { a = 1; a = 2; a = 3; }});
  while (true)
  {
    if (a == 1 && a == 2 && a == 3)
    {
      std::cout << "Hell World!" << std::endl;
    }
  }
}

Output:

Hell World!
Hell World!
Hell World!
Hell World!
Hell World!
Hell World!
Hell World!
Hell World!
Hell World!
Hell World!
Hell World!
Hell World!
Hell World!
Hell World!
Hell World!
Hell World!
Hell World!

...


January 2019

I think this question is highly relevant to this link -> C++11 introduced a standardized memory model. What does it mean? And how is it going to affect C++ programming?

Kiseong Yoo
  • 97
  • 1
  • 7

2 Answers2

10

You are experiencing undefined behavior.

Your code has a race condition; one thread is reading a while another is writing, and no synchronization occurs. You aren't allowed to do this in C++. Programs that do that can be compiled to do anything at all.

In fact, on a release build, I'd expect that to compile down to if(false). The compiler optimizes the main thread, notices no synchronization, proves that a cannot be 3 different values without UB, and optimizes out the if.

On a debug build, I could expect the symptoms you see. Not because it is more correct, but because debug builds tend not to trip over that kind of undefined behavior.

So the first thing you have to do to talk about your program reasonably is remove the undefined behavior: make a a std::atomic<int> instead of an int.

Now in both release and debug you'd expect to see ... exactly what your test showed. Or nothing. Or anything in between. The result is no longer undefined, but it remains non-deterministic.

The if statement isn't atomic. Between the conditions, a can change. And with a program running forever it should happen sometimes, as the other thread is changing it.

The resulting program is well defined. Even forward progress guarantees are ok, because you read an atomic variable.

Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524
4

You probably compiled your program with optimization disabled, so the assembly / machine-code for the target architecture you're compiling for actually does all the steps in the C++ abstract machine in the order they appear, including actually storing or loading to/from RAM. (Debug builds typically treat every variable as volatile.)

On typical architectures where a 32-bit int load/store is naturally atomic, a debug build behaves much like a portable C++ program with no undefined behaviour using std::atomic<int> a with a.store(1, std::memory_order_relaxed) (or on x86, std::memory_order_release).


The program starts a thread which repeatedly set the value a to 1, then 2, then 3. (This is the runThread line).

The main thread then tests whether a is equal to 1 and a is equal to 2 and a is equal to 3. And prints "hello world" if it is equal to all three. (this happens in the second while(true))

The reason why it does print "hello world" is because of concurrency. It happens that while one thread performs the three tests, the other thread writes the right values just at the right times. Remember that the three parts of the if are done one after each other


This is nowhere guaranteed to happen. It could be optimized out if you weren't making a debug build. But given the speed computers run at, and a typical implementation of threads, it does happen.

Do not rely on this behaviour. Rather, try to understand that the two threads perform operations concurrently.

Peter Cordes
  • 328,167
  • 45
  • 605
  • 847
Jeffrey
  • 11,063
  • 1
  • 21
  • 42
  • 2
    This answer explains one *possible* result of undefined behavior, but the program could do anything. I would actually expect the `if` condition to be optimized out for always being `false`, so it would never print. Though that's also just another possibility. – François Andrieux Oct 02 '18 at 17:30
  • 1
    Yeah, and I realized this forum is very strong about telling "this is UB, don't do it" strongly and consistently. I hoped it would be useful to the OP to learn _how_ it could possibly happen, though. Recognizing UB is one skill. Seeing possible thread flow is another. – Jeffrey Oct 02 '18 at 17:42
  • 1
    @FrançoisAndrieux: I updated this answer to explain why the UB turned into machine code that works this way, for a debug build on a "normal" CPU architecture with a normal compiler. Now it's correct, IMO, and gives the right context for its fairly good explanation of the nuts and bolts of the effect, that `a` can change between each step of the `&&` condition. – Peter Cordes Oct 03 '18 at 08:23