2

I was having the understanding that when two threads simultaneously read and write from/to the same variable, the application would crash. I wrote a small program to simulate this scenario. I ran it several times but do not notice any crash. What am I missing here?

#include <iostream>
#include <thread>

using namespace std;

int g_TestVar = 0;

void ReadFunction()
{
    for (size_t i = 0; i < 5000; ++i) {
        cout << this_thread::get_id() << "------" << g_TestVar << '\n';
    }
}

void WriteFunction()
{
    for (size_t i = 0; i < 5000; ++i) {
        g_TestVar = rand();
    }
}

int main()
{
    thread ReadThread[100];
    thread WriteThread[100];

    for (size_t i = 0; i < 100; ++i) {
        ReadThread[i] = thread(ReadFunction);
        WriteThread[i] = thread(WriteFunction);
    }

    for (size_t i = 0; i < 100; ++i) {
        ReadThread[i].join();
        WriteThread[i].join();
    }

    return 0;
}
Akshay CA
  • 43
  • 6
  • 4
    You're getting undefined behavior. There's no guarantee of what may happen in this case. Chances of getting incorrect data are high. Chances of an actual crash...much lower (in fact, I'd consider an actual crash a pretty strong indication of failure in the hardware design). – Jerry Coffin Jan 16 '21 at 08:20
  • 2
    [Undefined behavior can result in time travel](https://devblogs.microsoft.com/oldnewthing/20140627-00/?p=633) – user7860670 Jan 16 '21 at 08:29
  • Re: "any other problems other than a data race" -- as the old joke goes, "aside from that, Mrs. Lincoln, how did you like the play?" – Pete Becker Jan 16 '21 at 15:24

4 Answers4

3

As mentioned by Jerry Coffin, you are getting undefined behaviour. However, not all UB means you will get a crash. A crash typically happens because of two things:

  • Reading from/writing to a memory location that is not mapped into the program's memory space
  • Executing invalid instructions

Since the threads are executing valid code, and reading/writing from a variable that is clearly inside the program's memory space, you will not see a crash. It is UB since there is no guarantee that g_TestVar will be written to or read from atomically, although since you are just writing random values to it that doesn't matter anyway.

The problem with the program not crashing is of course that the undefined behavior is not apparent, and might give you a false sense of having a correct program. There are some tools, like Valgrind, and compiler options like -fsanitize-undefined that might help you find some forms of UB.

G. Sliepen
  • 7,637
  • 1
  • 15
  • 31
1

Your assumption about the program crashing because reading and writing concurrently from/to the variable is wrong.

Regarding atomicity... reading and writing operations to the variable could be atomic and independent from each other, but this depends on the architecture, so there is NO guarantee. Thanks to other contributors for clarifying this to me.

  • 4
    C++ does not guarantee that `int` read/writes are atomic. See [this](https://stackoverflow.com/questions/54188/are-c-reads-and-writes-of-an-int-atomic) for example. – dxiv Jan 16 '21 at 08:36
  • 1
    Even if you can read/write using only a single instruction, there is no guarantee that the actual read from/store to memory is atomic. For example, the memory bus width might be smaller than the CPU's register width, thus requiring multiple bus cycles for a single `int`. If there is no way to lock the bus during multi-cycle access, then atomicity is not guaranteed. It also depends whether the variable is correctly aligned. And x86 AVX load/store instructions are very large and not always atomic, see for example https://rigtorp.se/isatomic/. – G. Sliepen Jan 16 '21 at 08:58
  • 2
    Corrected my answer based on your clarification. Thanks. – Daniel Vazquez Jan 16 '21 at 09:18
1

You should make your variable g_TestVar an atomic variable like this:

#include <atomic>

std::atomic<int> g_TestVar = 0;

which will ensure each thread has atomic access to the variable. Otherwise it is undefined behaviour (not necessarily a crash).

You have to remember that there are a lot of layers between your source code and the actual machine instruction. There's hardware too including caches. All these things can reorder the memory reads and writes. All you care about is that your program executes in a sequential interleaved manner, but your compiler doesn't know what data is shared between threads. So it is possible that reads and writes don't happen in the order you want.

If you use an atomic variable or a mutex, you create a barrier or fence which tells the hardware to execute something in isolation.

jignatius
  • 6,304
  • 2
  • 15
  • 30
1

There are 3 issues I can think of with threads racing to read and write variable:

  1. If the variables is bigger than a word size, the variable might be partially written at the point it is read. This means the read will report an erroneous value.
  2. If the global variable is written and not read in the same thread, the compiler might decide to optimise the value out.
  3. Even if volatile, some processors may reorder reads and writes, this might cause issues if ordering of writes is important.
doron
  • 27,972
  • 12
  • 65
  • 103