(0,0,0),(1,0,0),(2,0,0),(3,0,0),(4,0,0),(5,7,0),(6,7,8),(7,9,8),(8,9,8),
(9,9,10)
(0,0,0),(0,1,0),(0,2,0),(1,3,5),(8,4,5),(8,5,5),(8,6,6),(8,7,9),(10,8,9),
(10,9,10)
(0,0,0),(0,0,1),(0,0,2),(0,0,3),(0,0,4),(0,0,5),(0,0,6),(0,0,7),(0,0,8),
(0,0,9)
(1,3,0),(2,3,0),(2,4,1),(3,6,4),(3,9,5),(5,10,6),(5,10,8),(5,10,10),
(9,10,10),(10,10,10)
(0,0,0),(0,0,0),(0,0,0),(6,3,7),(6,5,7),(7,7,7),(7,8,7),(8,8,7),(8,8,9),
(8,8,9)
Original listing from the book
Listing 5.6 Relaxed operations on multiple threads
Details
The book also states that
One possible output from this program is as follows:Output 5.6
Details
I believe, this output is impossible for reasons explained later. I do also believe that the explanation in the book might contain logical mistakes and errors in formulations that are non-conforming with the standard.
I will try to elaborate on a simplified example before going back to the original.
Simplified identical example
Let's reduce the number of threads to 2 and leave the writers only.
We will also reduce the number of loops to 2.
All we are interested in is a situation where the output produced looks like this:
Our statement is that this sequence, although showcased in a book as a possible output, and also reasoned as a possible outcome:
This statement rewritten for our simplified case reads as:
What's wrong with it? Let's take look closer
Circular dependency on a future value
And our simplified pseudo-code for reference:
T3 > T2 > T1
It is important to understand that for now these T1, T2 and T3 are thread-local and might be very different for these 2 threads. Which means, these are relative to thread timestamps and do not necessarily (actually, very unlikely) synchronized in "real" / global time.
At T1 thread_x:
y(already 1)x(as 1)Somewhere at T2 (in the view of thread_x) the x value is updated. It might be lagging a little for other threads, but it is happening AFTER it was written.
At some other point T1 thread_y:
x(already updated, 1)y(still 0, no one touched it yet, it's a 1st loop)y(as 1)This yields, in a view of this thread_y, which is guaranteed by the Standard to be sequential (at least in a view of THIS very thread) that reading of
xas 1 happens BEFORE writingyas 1.Now, a contradiction and an impossible circular dependency on a future value:
thread_x, which is sequential in the eyes of the thread_x, has read a value of
y1, which is being written as 1 only AFTER thread_y has read the value ofxas 1, which happens only AFTER thread_x has read a value ofyas 1. Blitz!In the very eyes of thread_x, we can read the value
yas 1 only AFTER the instruction of writingx. But the output shows the opposite.Meaning, it's invalid.
Important to note: we do not make any assumptions on how fast the threads synchronize: the relaxed memory ordering allows for the written values to be updated later than we expect and for threads to read the values that are lagging behind.
But the flow of one specific thread is guaranteed by the Standard inside of the very thread and the earlier read of a value that will written as a result of mutations performed by this very thread later on should not be possible.
Standard and supporting logical materials
I was surprised to find out that the text of the Standard itself is not freely available to the public, since everyone is referencing it, but due to impossibllity to find the original text I got a copy of a C++17 draft. I hope it's not too different from the original and I will be working with it.
https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/n4659.pdf
Let's list our important logical building blocks here used for a compilation of a later thesis, hidden, because a lot of text =):
Details
Back to original example
With all of this let's take a look at the part of the original output which is similarly problematic
Extracting the specific problematic part:
At the moment of
thread_xincrementingxfrom 5 to 6 it BEFORE readsyas 7 and only THEN writesxas 6At the moment of
thread_yincrementingyfrom 4 to 5 it BEFORE readsxas 8 and only THEN writesyas 5But at this point
thread_xhas already readyas 7 BEFORE writingxas even 6, butthread_yobserved a future value,xbeing already 8 BEFORE it didn't writeyas 5, not even talking of 7.These 2 are not just threads being unsynchonized: they show an impossible circular dependency on future values.
They do not just represent threads lagging in reading values, but in the view of the same thread reading values that have not yet been written and depend on this very thread to write them afterwards.
It's about sequence of actions in the very same thread.
Thread_x HAS to first read the value of y, which can only update AFTER the increment of x.
And thus, I conclude that this breaks the principles of the Standard and this output is impossible.
As for the statement in the book:
The conclusion is that it seems to be wrongfully formulated and that
Not any set of values that's consistent with variables, each holding the values 0 to 10 in turn, and that has the thread incrementing a given variable printing the values 0 to 9 for that variable, is valid.