[cpp-threads] Examples of cross-thread ordering
Jeremy Manson
jmanson at cs.purdue.edu
Sat Jul 15 06:16:30 BST 2006
Herb Sutter wrote:
> Thanks, Jeremy. I'm adding Shaz Qadeer who is interested in this and has
> some questions about JMM as well as C++MM.
>
> Jeremy Manson wrote:
>> Herb Sutter wrote:
>>
>>> Example 1:
>>>
>>> // thread T1
>>> x = 1; // a
>>> y = 2; // b
>>> r1 = y; // c
>>>
>>> // thread T2
>>> y = 3; // d
>>> x = 4; // e
>>> r2 = x; // f
>>>
>
> Can you explain a little more where this total order comes from, and how
> to analyze an example like this rigorously? One of the things that
> puzzles me about the JMM specification is that it partly relies on a
> happens-before relationship and partly doesn't.
I don't know what you mean by partly doesn't. I suspect that what is
confusing you is that the happens-before order is partly determined by
the total order I described, which is called the synchronization order.
Let's tackle happens-before and forget for the moment about the more
confusing causality rules.
So, if you have the example above, where x and y are volatile, legal
synchronization orders could include:
a,b,c,d,e,f
a,b,d,e,f,c
a,d,e,f,b,c
d,e,f,a,b,c
and so on and so forth.
Each volatile read must see the last volatile write in the
synchronization order. So if we have a,d,e,f,b,c, then f must see the 4
written by e, and c must see the 2 written at b.
Synchronization order must reflect program order, as well. Therefore,
if you have b, d, c in the synchronization order, that means that you
must also have a -> b -> d -> c. e -> f must come after d in the
synchronization order, so you can have:
a, b, d, e, f, c
a, b, d, e, c, f
a, b, d, c, e, f
In all of these, the read at f returns 4.
>>> Example 2:
>>>
>>> // thread T1
>>> x = 1; // a
>>> r1 = y; // b
>>> r2 = y; // c
>>>
>>> // thread T2
>>> y = 2; // d
>>> z = 6;
>>>
>>> // thread T3
>>> y = 4; // e
>>> r3 = z;
>>> x = 8; // f
>>> r4 = x; // g
>>>
>>> If r1 == 2 and r2 == 4 (and r3 == 6), is it possible to have r4 ==
> 1?
>> I don't see how; it doesn't look as if there is a total order that
> would
>> reflect that.
>
> Sure, that's the intuitive result, but again can I reason about that
> rigorously using happens-before? For example, can I mechanically apply
> the JMM rules to say that if r1==2 and r2==4 then d->b->e->c, and so
> because b->e therefore a->f?
Sounds right to me. I'm sorry if I wasn't a little clearer on that point.
>>>
>>> Example 3:
>>>
>>> // thread T1
>>> x = 1; // a
>>> y = 3; // b
>>> r1 = y; // c
>>>
>>> // thread T2
>>> y = 4; // d
>>> r2 = x; // e
>>>
>>> // thread T3
>>> x = 2; // f
>>>
>>> If r1 == 4 and r2 == 2, what if anything can we say about a
>>> happens-before relationship between lines a and f?
>>
>> Not a lot. If -> is the happens-before relationship, we can say a ->
> b
>> -> c, d -> e, f -> e and d -> c. If there is no other code creating
>> happens-before relationships, then we can say that there isn't a
>> happens-before relationship between the two.
>>
>> This is because there are two factors at work here in Java -- a total
>> order over accesses to volatiles, and the happens-before order, which
> is
>> determined based on program order and on which volatile read sees
> which
>> volatile write. In the total order, line a has to occur before line
> f,
>> because otherwise, line e wouldn't see the value 2. This implies
>> nothing for the happens-before order, though, because a and f are both
>> writes.
>
> Why is there a distinction between these two orders, and does there need
> to be? Is there a unified order one can systematically apply?
>
Given the synchronization order, you can determine what we call the
"synchronizes-with order". Each volatile write "synchronizes-with" all
subsequent volatile reads of the same variable in the synchronization
order. So, if we have a,d,e,f,b,c, then you have a -sw-> f, e -sw-> f,
b -sw-> c.
The synchronizes-with order composes with program order to produce the
happens-before order. Note that the happens-before order is a partial
order. A normal (i.e., non-volatile, non-final) read can return the
value written by any write as long as:
a) there was no intervening write to that variable in the happens-before
order, and
b) the read does not happen-before the write.
So, let's say we have normal variable z, and volatile x:
Thread 1:
1: z = 0;
2: z = 1;
3: x = 1;
Thread 2:
4: r1 = x;
5: if (r1 == 1) {
6: r2 = z;
7: z = 2;
8: }
Thread 3:
9: z = 3;
Let's assume the read of z in line 6 occurs. That means that the read
of x in line 4 returned 1. That means line 3 occurred before line 4 in
the synchronization order. We can draw a synchronizes-with arrow from
line 3 to line 4. So, now we know the happens-before relationships in
this program. Basically, all of Thread 1 happens-before all of Thread 2.
Which writes can the read at line 6 see? All of those that meet point
a) and b) above. That includes the writes at lines 2 and 9. It does
not include the write at line 7, because that violates point b). It
does not include the write at line 1, because that violates point a).
This result should be reasonably intuitive, but perhaps the
formalization of it takes a while to grasp. I think of it in terms of a
partial order over actions in an execution, which I think ends up being
the most intuitive way of thinking about it once you understand it.
To answer your actual question, then, the synchronization order
determines which values are read and written by volatile variables,
while the happens-before order, which is determined partially by the
synchronization order, determines the values read and written by normal
variables.
This is all ignoring the issue of causality, but it is important to
understand how happens-before works in the model before understanding
causality.
Hope that helps. It is probably as clear as mud. I would be happy to
clear up other questions you have.
Jeremy
More information about the cpp-threads
mailing list