I am trying to figure out how SPIN chooses the order in which to execute and terminate processes in the following example. I realize that a main focus of SPIN is analyzing concurrent processes, but for my purposes I am just interested in simple linear execution. In the following example I just want step1() then step2() to be executed in that order.
int globA;
int globB;
proctype step1() {
atomic {
globA = 1;
}
}
proctype step2() {
atomic {
globB = 2;
}
}
init {
atomic {
run step1();
run step2();
}
}
However, even with the atomic {} declarations, the processes become interleaved in their execution. Using the command spin -p my_pml_code.pml
I get the following 3 outputs only (I ran it many times and these were the only outputs).
Run 1:
0: proc - (:root:) creates proc 0 (:init:)
Starting step1 with pid 1
1: proc 0 (:init::1) creates proc 1 (step1)
1: proc 0 (:init::1) pml_testing/transition_testing.pml:16 (state 1) [(run step1())]
Starting step2 with pid 2
2: proc 0 (:init::1) creates proc 2 (step2)
2: proc 0 (:init::1) pml_testing/transition_testing.pml:17 (state 2) [(run step2())]
3: proc 2 (step2:1) pml_testing/transition_testing.pml:11 (state 1) [globB = 2]
4: proc 1 (step1:1) pml_testing/transition_testing.pml:6 (state 1) [globA = 1]
4: proc 2 (step2:1) terminates
4: proc 1 (step1:1) terminates
4: proc 0 (:init::1) terminates
3 processes created
The order is proc 0 -> 0 -> 0 -> 0 ->2 -> 1 -> 2 -> 1 -> 0
Run 2:
0: proc - (:root:) creates proc 0 (:init:)
Starting step1 with pid 1
1: proc 0 (:init::1) creates proc 1 (step1)
1: proc 0 (:init::1) pml_testing/transition_testing.pml:16 (state 1) [(run step1())]
Starting step2 with pid 2
2: proc 0 (:init::1) creates proc 2 (step2)
2: proc 0 (:init::1) pml_testing/transition_testing.pml:17 (state 2) [(run step2())]
3: proc 1 (step1:1) pml_testing/transition_testing.pml:6 (state 1) [globA = 1]
4: proc 2 (step2:1) pml_testing/transition_testing.pml:11 (state 1) [globB = 2]
4: proc 2 (step2:1) terminates
4: proc 1 (step1:1) terminates
4: proc 0 (:init::1) terminates
3 processes created
The order is proc 0 -> 0 -> 0 -> 0 -> 1 -> 2 -> 2 -> 1 -> 0
Run 3:
0: proc - (:root:) creates proc 0 (:init:)
Starting step1 with pid 1
1: proc 0 (:init::1) creates proc 1 (step1)
1: proc 0 (:init::1) pml_testing/transition_testing.pml:16 (state 1) [(run step1())]
Starting step2 with pid 2
2: proc 0 (:init::1) creates proc 2 (step2)
2: proc 0 (:init::1) pml_testing/transition_testing.pml:17 (state 2) [(run step2())]
3: proc 2 (step2:1) pml_testing/transition_testing.pml:11 (state 1) [globB = 2]
3: proc 2 (step2:1) terminates
4: proc 1 (step1:1) pml_testing/transition_testing.pml:6 (state 1) [globA = 1]
4: proc 1 (step1:1) terminates
4: proc 0 (:init::1) terminates
3 processes created
The order is proc 0 -> 0 -> 0 -> 0 ->2 -> 2 -> 1 -> 1 -> 0
The output I am trying to get it simply: proc 0 -> 0 -> 0 -> 0 ->1 -> 1 -> 2 -> 2 -> 0
I realize that proc 0 can't terminate until 1 and 2 have terminated, but why are the terminations of 2 and 1 being interleaved non-deterministicly? Any why is SPIN randomly choosing between executing proc 1 and proc 2 when the init function is atomic and therefore should be executed in order? And why can I get proc 2 to start and terminate before proc 1 (in Run 3) but not the other way around?
Note: I have also tested this using dstep
instead of atomic
and I get the same result.
First, let me try give short answers to each of your questions:
The processes always terminate in a deterministic fashion:
2
terminates before1
,1
before0
and0
is always the last one. However, there is nothing special about process termination: it is simply the final transition that is taken by a process. As a result, process interleaving is possible at any point in time in which there are more than one process with an (immediately) executable instruction.Although it is true that
init
executes both of his instructions atomically, the important fact to keep in mind is thatstep1
andstep2
are independent processes and are executed afterinit
exits its atomic block:run
is not a function call, it just spawns a process inside the environment with absolutely no guarantee that such process is executed immediately. That might or might not happen depending on whether the spawned process has any executable instruction, whether the process that is currently executing is in an atomic sequence and on the outcome of the non-deterministic scheduler process selection.In Promela, processes can only die in reverse order of their creation, as indicated in the docs:
Therefore,
2
can terminate before1
because it has higher_pid
value, whereas1
must wait upon2
to be terminate before it can die.There is no such thing as an atomic process if you have more than one in your system. Even if you enclose the whole body of a process inside the atomic keyword, the termination step is still outside the atomic block. The scheduler never interrupts a process executing an atomic sequence, unless the process blocks in front of an instruction that is not executable. In such a case atomicity is lost and any other process can be scheduled for execution, as described in the docs:
In your question you state that your goal is to obtain the following execution-flow:
In your code example, this execution-flow is forbidden because it makes process
1
terminate before process2
and this is not allowed by the rules (see the answer to your third question).No statement inside your atomic block can block, therefore there is absolutely no difference among using
d_step
oratomic
in your code. However, I invite you to read this answer to get an insight of similarities and differences among atomic and d_step.EXAMPLE EXECUTION FLOW:
Second, let me try a deeper answer level based on the analysis of the execution-flow.
In your code example there are three processes.
init
is (always) the first process to be created (when available), and for this reason it is (always) assigned_pid
equal to0
and scheduled for first. Since the whole body of theinit
process is enclosed in an atomic block, this process executesrun step1();
andrun step2()
without interleaving with other processes. Processstep1
is assigned_pid
equal1
, because it's the second process to be created, whereas processstep2
is assigned_pid
equal2
, since it's the third process to be created.In your example, processes
step1
andstep2
cannot be scheduled for execution until when theinit
process reaches the end of the atomic, which in your code example coincides with the end of theinit
code.When
init
reaches the end of its body, the process (with_pid
equal to0
) cannot die yet because inside the environment there is at least one process with a_pid
value greater than his own, namelystep1
andstep2
. Althoughinit
is blocked, bothstep1
andstep2
are ready for execution, so the non-deterministic scheduler can arbitrarily select eitherstep1
orstep2
for execution.If
step1
is scheduled first, then it executes its only instructionglobA = 1;
without interleaving withstep2
. Notice that, since there is only one instruction inside the atomic block and this instruction is atomic on his own, the atomic block is redundant (the same holds forstep2
). Again, sincestep1
has_pid
equal to1
and there is still a process with higher_pid
value around, processstep1
cannot die yet. At this point, the only process that can be scheduled for execution isstep2
, which can also terminate because there is no process with higher_pid
value. This allowsstep1
to terminate, which in turn allowsinit
to die as well. This execution-flow corresponds to your run 2.If
step2
is scheduled first, then once this process has assigned the value2
toglobB
and reaches the end of its body, which is outside the atomic block, there are two possible execution flows:case A) the scheduler non-deterministically selects
step2
to execute again, andstep2
terminates; now the only available option for the scheduler is to makestep1
execute its own instruction, make it terminate and then allowinit
to terminate too. This execution flow corresponds to run 1.case B) the scheduler non-deterministically selects
step1
to execute,step1
assigns1
toglobA
but cannot terminate becausestep2
is still alive; the only process that can be scheduled isstep2
, so the latter terminates after being selected by the scheduler, allowingstep1
andinit
to terminate as well in cascade. This execution flow corresponds to run 3.LINEAR EXECUTION:
The simplest and most obvious way to achieve linear execution is to have only one process inside your model. It is trivial to see why this is the case. So your example would become:
Having atomic blocks in this code is no longer necessary, since there is only one process. Of course, you might frown upon such a trivial solution, so let's see another solution based on a global variable:
Differently than your code example, here
globB = 2
can never be executed beforeglobA = 1
is executed thanks to the synchronisation variableterminated
. However, similarly to your code example, the actual termination of processesstep1
andstep2
is subject to interleaving. i.e. ifstep1
terminates immediately, so thatstep2
is created only afterstep1
has completely released the resources it owns, thenstep2
is assigned_pid
equal to1
; otherwise,step2
is assigned_pid
equal to2
.The best solution I can think of is based on the concept of message passing. Basically, the idea is to allow only the process that currently holds a token to be scheduled at any given point in time, and to pass around such token in the desired scheduling order:
Notice that this solution forces only one possible scheduling. This can be verified by running an interactive simulation:
As you can see, the user is never offered the chance to make any choice, because there is only one possible execution-flow. This is obviously due to the fact that A) I did not put any instruction before
in!TOKEN
and afterout!TOKEN
B) the desired scheduling order coincides with the order in which processes are created.