Does simultaneous read/write cause any other problems other than data race?

Viewed 426

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;
}
4 Answers

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.

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.

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.

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.
Related