c++c++11language-lawyerlock-freestdatomic

C++ Multi-threading: Visible side-effects of non-atomic variables


There is a part in the C++ standard about multi-threading memory model that I don't understand.

A visible side effect A on a scalar object or bit-field M with respect to a value computation B of M satisfies the conditions:

  • A happens before B and

  • there is no other side effect X to M such that A happens before X and X happens before B.

The value of a non-atomic scalar object or bit-field M, as determined by evaluation B, shall be the value stored by the visible side effect A.

And also according to the C++ standard, a "happens before" relationship between threads must be established by "synchronizes with" or "is dependency-ordered before", so a "happens before" relationship will not be established without inter-thread synchronization.

Now suppose there are two threads T1 and T2, both started by the main thread and never do any synchronization with each other (so there would not be any "happens before" relationships established between T1 and T2). If T1 writes to a non-atomic variable M, then according to the quote above, T2 should never see M modified by T1, because there is no "happens before" relationship between T1 and T2.

Instead, T2 has "synchronizes with" relationship established with the main thread at the time T2 starts, so T2 should see the value of M set by the main thread before it was started by the main thread, because there is a "happens before" relationship between the main thread and T2.

Right? However I did an experiment on my machine and it was not the case. What is wrong?


Solution

  • T1 writes to a non-atomic variable M, then according to the quote above, T2 should never see M modified by T1

    Consider the following:

    Two actions are potentially concurrent if

    • they are performed by different threads, or

    • they are unsequenced, at least one is performed by a signal handler, and they are not both performed by the same signal handler invocation.

    T2's read of M and T1's write to M are "potentially concurrent". Next:

    Two expression evaluations conflict if one of them modifies a memory location and the other one reads or modifies the same memory location.

    T2's read of M conflicts with T1's write to M. So these "potentially concurrent" actions conflict.

    Lastly, we come to:

    The execution of a program contains a data race if it contains two potentially concurrent conflicting actions, at least one of which is not atomic, and neither happens before the other, except for the special case for signal handlers described below. Any such data race results in undefined behavior.

    T2's read of M does not happen before T1's write to M, nor does T1's write to M happen before T2's read of M. Therefore, you have a data race.

    And data races are undefined behavior. It's not that T2 won't see the write to M; it's that it could see anything: the old value, the new value, some third value, nasal daemons issuing forth, anything.