The problem with data races is not, that you can read a wrong value on a machine level. The problem with data races is, that both compiler and processor perform a lot of optimizations on the code. To make sure that these optimizations are correct in the presence of multiple threads, they need additional information about variables that can be shared between threads. Such optimizations can for example:
- reorder operations
- add additional load and store operations
- remove load and store operations
There is a good paper benign data races by Hans Boehm called How to miscompile programs with "benign" data races. The following excerpt is taken from this paper:
Double checks for lazy initialization
This is well-known to be incorrect at the source-code
level. A typical use case looks something like
if (!init_flag) {
lock();
if (!init_flag) {
my_data = ...;
init_flag = true;
}
unlock();
}
tmp = my_data;
Nothing prevents an optimizing compiler from either reordering the setting of
my_data
with that of init_flag
, or even from advancing the load of my_data
to before the first test of init_flag
, reloading it in the conditional if init_flag
was not set. Some non-x86 hardware can perform similar reorderings even if the compiler
performs no transformation. Either of these can result in the final read of my_data
seeing an uninitialized value and producing incorrect results.
Here is another example, where int x
is a shared and int r
is a local variable.
int r = x;
if (r == 0)
printf("foo\n");
if (r != 0)
printf("bar\n");
If we would only say, that reading x
leads to an undefined value, then the program would either print "foo" or "bar". But if the compiler transform the code as follows, the program might also print both strings or none of them.
if (x == 0)
printf("foo\n");
if (x != 0)
printf("bar\n");