How is it possible for these 2 functions, executed in parallel, to output an unlikely result?

Viewed 90

I have these 2 functions, executed in parallel :

int x = 10 // a common variable

void P1(){                   void P2(){
   while(1){                    while(1){
   x--;                         x--;
   x++;                         x++;
   if(x!=10)                    if(x!=10)
      printf("x=%d",x);             printf("x=%d",x);
   }                            }
}                             }

parbegin P1(); P2();
parend

The functions occasionally output x=8, which does not look possible.

Does it have to do with the fact that the x-- and x++ operations are actually translated into 3 instructions in machine-code ( see the symbolic assembly decode )?

LD    R0, X             // LOAD into R0 the value of x
INCR  R0 (or DECR R0)   // { INCR | DECR }-increase/decrease the value of R0 by 1
STO   R0, X             // STORE the new value back to x

If yes,
then what is the order of the instructions, so that the output "x=8" was generated?

If not,
then why does it occasionally print x=8?

3 Answers

It is entirely possible, and it is precisely due to the reason that you suspect, but not exactly in the order that you suspect.

The x-- statement is not atomic, it consists of a load operation, a decrement operation, and a store operation. So, if the x-- gets pre-empted by the other thread, the result can be as if the x-- statement did not execute at all. But that would not yield an 8, it would actually cause x to not reach a value as low as 8.

However, the same holds true for the x++ statement: pre-emption may cause an x++ to never happen, so the result in that case would be that x would fail to exceed 9, so it would then take only one successful x-- to bring it down to 8.

So, if more x++s are cancelled than x--s, then it is entirely possible that x would become 8. Actually, in your code sample it is entirely possible that x could receive any value you can think of.

Yes, it's because x++ and x-- are not atomic operations. If one of these operations is preempted, another thread might have changed the value of x, so the thread doing the increment or decrement operation stores a result that's computed from a stale value of x.

Here's a possible interleaving that leads to printing x=9, x=8, x=9. In this particular interleaving, the increment operations in P1 and P2 start from the same value of x, and therefore they both write the same result, so that x is effectively only incremented once.

P1          P2          x
                        10
LD R0, x                10
DECR R0                 10
ST R0, x                10→9
            LD R0, x    9
            DECR R0     9
            ST R0, x    9→8
            LD R0, x    8
LD R0, x                8
INCR R0                 8
ST R0, x                8→9
            INCR R0     9
            ST R0, x    9→9
            print       9
            GOTO loop   9
            LD R0, x    9
            DECR R0     9
            ST R0, x    8
print                   8
GOTO loop
            LD R0, x    8
            INCR R0     8
            ST R0, x    8→9
            print       9
            GOTO loop   9

Note that in this interleaving, the loop body is executed three times (once in P1 and once in P2), and the final value of x is not 10. A similar sequence could repeat and the value of x could end up being arbitrarily low. Symmetrically, it could end up being arbitrarily high.

Here's another interleaving where 8 is reached with a single iteration in each thread. In this one, the decrement operation in P2 effectively cancels the increment in P2. With this interleaving, the very first output is x=8, followed by x=9.

P1          P2          x
                        10
LD R0, x                10
DECR R0                 10
ST R0, x                10→9
            LD R0, x    9
            DECR R0     9
LD R0, x                9
INCR R0                 9
ST R0, x                9→10
            ST R0, x    10→8
            LD R0, x    8
print                   8
GOTO loop
            INCR R0     8
            ST R0, x    8→9
            print       9
            GOTO loop   9

Here's yet another interleaving where the output starts with x=8, x=7, x=6. Here the increment operations all happen to be effectively cancelled by a concurrent decrement. This could go on forever.

P1          P2          x
                        10
LD R0, x                10
DECR R0                 10
ST R0, x                10→9
            LD R0, x    9
            DECR R0     9
LD R0, x                9
INCR R0                 9
ST R0, x                9→10
            ST R0, x    10→8
            LD R0, x    8
print                   8
GOTO loop               8
LD R0, x                8
DECR R0                 8
            INCR R0     8
            ST R0, x    8→9
ST R0, x                9→7
LD R0, x                7
            print       7
            GOTO loop   7
            LD R0, x    7
INCR R0                 7
ST R0, x                8
            DECR R0     8
            ST R0, x    8→6
print                   6
GOTO loop               6

Q : " ... why does it occasionally print x=8? "

Because this is a very example of the by-product of executing un-coordinated concurrent operations in a CRCW-PRAM-abstract computing machine, as was explained in elementary theory of concurrent-processing :

parbegin P1(); P2();
parend;

as defined above does nothing else but unrestricted and principally colliding CRCW-operations on shared x ( i.e. principally - so independently from what kind of a compiler was thought to be used, what compilation tricks were thought to have been put in-place, and what target silicon was deemed to be used for loading & running such a code-execution product ( Be it RISC/CISC? Be it superscalar or not? Be it pipelined? Be it in-order/out-of-order executing? etc.) -- the problem starts from how the imperatively postulated code was designed (as it was), not by how the code-representation & transformation thereof did evolve or how the target silicon-device actually operates at bottom-most level at the end.

The responsibility for collision-avoidance is & always was on the author's side, wasn't it?

The code, designed as-is, above "shares" access to x, both on Read and Write, constrained by nothing - thus principally free-to-collide whenever externally influenced "order-of-steps" gets skewed ( not meeting an expected sequence of steps [do +1 <_almost_right_after_> -1 was done], deemed to maintain x=10 invariant ( which it is not capable of, even in abstract, perfectly synchronous, ideal CRCW-PRAM machine )

enter image description here

As has been explained ages ago, in Bernstein’s conditions (see Theory and examples ), all just-[CONCURRENT] ( the more the True-[PARALLEL] ) code-execution units must strictly control order in which shared variables ( the x, in the mock-up code above) are read and updated.

Related