Reputation: 122133
I am trying to understand where is the line between a data race and no date race and what are the consequences concerning undefined behavior.
Consider this example:
#include <chrono>
#include <thread>
#include <cstdlib>
#include <iostream>
#include <ctime>
#include <functional>
void write(int delay, int& value, int target) {
std::this_thread::sleep_for(std::chrono::milliseconds(delay));
value = target;
}
int main() {
int x;
std::srand(std::time(nullptr));
auto t1 = std::thread(write, rand()%100, std::ref(x), 42);
auto t2 = std::thread(write, rand()%100, std::ref(x), 24);
t1.join();
t2.join();
std::cout << x;
}
Does this code have a data race always, or only sometimes? Is the behavior of the above code undefined according to the standard always, or only sometimes (depending on the outcome of rand()
)?
PS: Of course I cannot know if the output will be 42
or 24
, but in the presence of undefined behavior I would not even expect either of the two with certainty, it could be 123
or "your cat ate my fish"
.
PPS: I do not care for high quality randomness, hence rand()
is fine for this example.
Upvotes: 0
Views: 295
Reputation: 81105
Many platforms can at essentially zero cost offer useful behavioral guarantees stronger than the Standard mandates, but unfortunately the Standard provides no means by which a program can determine what guarantees are available, and optimizers may rearrange code without regard for whether that would break programs that exploit guarantees the platform would otherwise provide at essentially zero cost.
For example, given int x, y, *p;
, consider the following code snippet:
x = 0x0123;
y = *p;
... maybe some computations here that use up CPU registers
x = 0x0124;
An implementation might notice that between the stores of 0x0123
and 0x0124
, the value of x
is read but not written, and it would thus be possible to change the latter write from a sequence like mov ax,0124h / mov _x,ax
or mov word [_x],0124h
(on the 8088, either sequence would be six bytes plus two memory operations) to inc byte _x
(four bytes plus two memory operations). Such code, however, might malfunction badly if there is an intervening write of some other value like 0x00FF. Downstream code might regard any non-zero value of x
as equally acceptable, and the source code has never requested that any value be written whose bottom byte is zero, but if the outside write stores 0x00FF just before the inc byte _x
instruction, x
could be left holding zero, quite unexpectedly.
In many cases, the cost of refraining from such optimizations would be less than the cost of including memory barriers sufficient to prevent data races, but unfortunately there is no nice means of specifying such semantics unless one replaces x
with an "atomic int", which might require extra storage, and changes all accesses to x
to explicitly use relaxed semantics.
Upvotes: 1
Reputation: 7973
On architectures that can store a value to an int
in a single bus cycle, you would either get 42
or 24
as the output, never 123
or any other value. However, theoretically you could have a multi-core processor where int
is larger than the native data width, requiring multiple stores, in which case the stores of the two threads might interleave. This actually happens in practice on 32-bit processors when trying to store to a uint64_t
.
Upvotes: 2
Reputation: 68561
This code always has a data race. There is no happens-before ordering between the two writes, so there is a data race.
The OS could in principle schedule the two threads so that both sleeps returned at the same time, as long as each sleep was at least as long as the specified delay time.
Upvotes: 8