HAPPENS-BEFORE
"Happens before" is a partial order describing program events,
invented by Leslie Lamport.
Consider multithreaded executions as traces R of events E, as defined
below. (A trace is just a sequence.)
Events E ::= start(T)
| end(T)
| read(T,x,v)
| write(T,x,v)
| spawn(T1,T2)
| join(T1,T2)
| lock(T,x)
| unlock(T,x)
Here T is a thread identifier, x is a variable, and v is a value. So
the event read(T,x,v) indicates that thread T read value v from
variable x. We also assume that traces R are well-formed by requiring
the first event by a thread T in R must be start(T). No events by T
may follow end(T) in the trace.
Let E1 < E2 be the ordering of events as they appear in the trace,
which is transitive, irreflexive, and antisymmetric, as usual. Define
happens-before ordering <: in a trace R as follows: E1 <: E2 iff E1 < E2
and one of the following holds:
a) thread(E1) = thread(E2)
b) E1 is spawn(T1,T2), and E2 is start(T2)
c) E2 is join(T1,T2), and E1 is end(T2)
d) E1 is unlock(T1,x) and E2 lock(T2,x)
e) there exists E3 with E1 <: E3 and E3 <: E2 (i.e., the
happens-before ordering is transitive)
VISIBILITY
Given EW == write(T1,x,v1) and ER == read(T2,x,v2) in trace R, we have
that EW "is not visible" to ER (i.e., v1 != v2) if
a) ER <: EW (i.e., the read happens before the write)
b) there exists some intervening event EW2 == write(T,x,v3) such
that EW <: EW2 <: R (i.e., the first write is overwritten by
the second)
Otherwise EW is visible at ER, and thus the read could "see" the value
written in EW. Here are some examples.
(starting with x = 0)
Thread 1: Thread 2:
x = 1; y = x;
y = 2;
Here are some possible traces:
R1 == write(T1,x,1); read(T2,x,0); write(T2,y,0); write(T1,y,2)
R2 == write(T1,x,1); read(T2,x,1); write(T2,y,1); write(T1,y,2)
R3 == read(T2,x,0); write(T1,x,1); write(T2,y,0); write(T1,y,2)
R4 == write(T1,x,1); read(T2,x,1); write(T1,y,2); write(T2,y,1)
R5 == read(T2,x,0); write(T1,x,1); write(T1,y,2); write(T2,y,0)
Notice that in trace R1, the read by T2 for x got value 0; this was
legal because values x=1 and x=0 are visible at that point in the
trace; this is because the write by T1 to x does not "happen before"
the read. Trace R2 reads 1. In trace R3, the read by T2 occurs
first, so the only possible value it can read is the initial value for
x, which is 0. Traces R4 and R5 are the same as R2 and R3, resp., but
with the last two events swapped. Notice that these traces show that
the final result of y, when running this program, can be 0, 1, or 2.
Here is another example.
(starting with x = 0)
Thread 1: Thread 2:
lock(y); lock(y);
x = 1; x = x + 1;
unlock(y); unlock(y);
Here are the two possible traces:
R1 == lock(T1,y); write(T1,x,1); unlock(T1,y);
lock(T2,y); read(T2,x,1); write(T2,x,2); unlock(T2,y)
R2 == lock(T2,y); read(T2,x,0); write(T2,x,1); unlock(T2,y)
lock(T1,y); write(T1,x,1); unlock(T1,y);
In the first trace, notice that the read by T2 reads 1 from x. This
is because the the write(T1,x,1) happens before read(T2,x,1)---these
two events are ordered thanks to the fact that unlock(T1,y) <:
lock(T2,y) and transitivity. It is not possible for T2 to have read 0
at this point. The other trace has thread T2 acquire the lock first.
DATA RACES
With the happens-before ordering, we can precisely define a data race:
A data race takes place when two events in trace R, at least one is a write,
access the same memory location, and they are unordered according to
happens-before. Here are some examples, the first is the same example
we saw above:
(starting with x = 0)
Thread 1: Thread 2:
x = 1; y = x;
y = 2;
For any possible execution of these two threads, the writes to x and y
in Thread 1 are not ordered with the write and read of y and x, resp.,
in Thread 2. Thus they constitute data races. Here is an example
trace, in which E11 == write(T1,x,1), E21 == read(T2,x,0), E22 ==
write(T2,y,0), and E12 = write(T1,y,2):
Trace R == E11; E21; E22; E12
In this trace E11 and E21 constitute a data race: they are not ordered
by happens-before (since they are in different threads with
synchronization in between), and access the same variable, with the
E11 event being a write. Other data races are between E11 and E22,
E12 and E21 and E12 and E22.
Here is another example:
Thread 1: Thread 2:
lock y; print(x);
x = 1;
unlock y;
Here we also have a data race between the read in thread 2 (to do the
printing) and the write in thread 1. Here is a possible trace:
lock(T1,y); write(T1,x,1); unlock(T1,y); read(T2,x,0)
In this trace we have a data race between the write(T1,x,1) event and
the read(T2,x,1) event because these events are not ordered by
happens-before. This is because, despite the fact that they are
ordered in the trace (by the < order), they are in different threads
and have no synchronization events between them. All possible traces
of this program exhibit this race. Notice that for the trace above,
the read and write event do not happen "at the same time" -- they are
simply unordered in the trace.
Here is a program that sometimes exhibits races:
Thread 1: Thread 2:
x = 1; lock y;
lock y; unlock y;
unlock y; print(x);
In this case, the following trace does not have a data race:
write(T1,x,1); lock(T1,y); unlock(T1,y); lock(T2,y); unlock(T2,y); read(T2,x,1)
In this case, the write and the read are ordered by happens before
because unlock(T1,y) <: lock(T2,y) according to the rules above, all
other events are ordered by the trace order < and so with transitivity
we have that write(T1,x,1) <: read(T2,x,1) so there is no race.
Here is a trace that does exhibit a race:
lock(T2,y); unlock(T2,y); write(T1,x,1); read(T2,x,0); lock(T1,y); unlock(T1,y)
Here, the write and read events are not ordered by happens-before, and
thus constitute a race.