3

Let's say I have two threads that randomly increment or decrement from a static int variable in the global scope. My program is not concerned with the exact value of this variable, only whether it is generally increasing or decreasing over-time.

Though I have written some assembly code during college, I am not familiar with how CPUs deal with multithreading and communicate between cores. Is there any chance that two simultaneous writes can corrupt a global variable on a per-byte level? Or are reads and writes to memory (e.g. move, load, store) always atomic?

I use Visual Studio 2022 and C++17, and am hoping to target all modern desktop CPUs (intel, AMD).

Michael Sohnen
  • 953
  • 7
  • 15
  • 1
    Undefined behavior means anything can happen. – Stephen Newell Apr 20 '22 at 00:35
  • 1
    C++ is not a glorified assembler. If you have a data race, you have undefined behaviour. If you have undefined behaviour, your program will explode in your face. – Passer By Apr 20 '22 at 00:37
  • 1
    When dabbling in undefined behavior, it’s not only the hardware you have to worry about, but also the compiler’s optimizer. The optimizer can and will take advantage of the fact that undefined behavior is forbidden to make optimizations that assume that undefined behavior doesn’t ever occur… which means that if undefined behavior does occur, you are likely to see your program exhibit “interesting” behavior you wouldn’t expect just from reading the c++ source. – Jeremy Friesner Apr 20 '22 at 00:41
  • Does this mean that you cannot implement a mutex using a simple global integer or boolean value? – Michael Sohnen Apr 20 '22 at 02:18
  • @MichaelSohnen *Does this mean that you cannot implement a mutex using a simple global integer or boolean value?* You need to use the operating system primitives for this. A mere `bool` is insufficient. Fortunately, C++11 and beyond has wrapped this into the language itself, using `std::mutex`, `std::atomic`, etc. – PaulMcKenzie Apr 20 '22 at 02:32
  • Also, [read about the as-if rule](https://stackoverflow.com/questions/15718262/what-exactly-is-the-as-if-rule). – PaulMcKenzie Apr 20 '22 at 02:38
  • @PaulMcKenzie Thank you for the clarification. I had some success with std::mutex, but I could not get my program to work reliably. I reverted my code to single-threaded and am going to learn more about thread synchronization before coming back to it. – Michael Sohnen Apr 20 '22 at 03:22
  • You might appreciate some of the examples over at https://stackoverflow.com/questions/71866535/which-types-on-a-64-bit-computer-are-naturally-atomic-in-gnu-c-and-gnu-c-mea, especially the last one in which trying to use `bool` as a semaphore could yield impossible results. – Nate Eldredge Apr 20 '22 at 06:41
  • The rule on x86-64, at the level of assembly, is that a single load or store instruction of up to 64 bits, if naturally aligned, is atomic. But at the level of C++, there is no guarantee that for instance `bool flag; tmp = flag;` will actually *execute* a single load instruction. As Jeremy Friesner says, the compiler is entitled to optimize it in "interesting" ways that may completely break in the presence of concurrent access. The idea is that if you can't accept such optimizations then you use `std::atomic`. – Nate Eldredge Apr 20 '22 at 06:48

1 Answers1

5

The CPU is the least of your problems here. C++ states that any of these interactions represents undefined behavior. As such, consider the following code:

int i; //Global variable.

void some_func()
{
  i = 5;

  if(i > 5)
  {
    //A
  }
}

The compiler can see all of the code between assigning to i and checking its value. It can see that there is no inter-thread synchronization between these two operations. Because the C++ standard states that any modifications to i from another thread under these circumstances yield undefined behavior, the compiler is free to assume that such modifications do not happen.

Therefore, the compiler knows that i will never be greater than 5 at this point and therefore may remove the block "A" and conditional test, not even emitting assembly that could be executed.

That is what "undefined behavior" means.

If you want to play around with things like that, if you want to actually use the behavior of your CPU/cache/etc in such scenarios, you're going to need to operate in a lower-level language. And for what it's worth, the C standard has more-or-less the same wording, so C ain't it.

So long as you are working in C or C++, the question you are asking is fundamentally unanswerable.

Nicol Bolas
  • 449,505
  • 63
  • 781
  • 982