We start with a series of examples illustrating race conditions.
Example 1: A Tiny First Program
1 /* A variable shared between all processes. */
2 show int bal = 0;
3
4 active proctype deposit()
5 {
6 bal++;
7 }
8
9 active proctype withdraw()
10 {
11 bal--;
12 }
We have two threads, one running deposit
and one running withdraw.
The proctype keyword specifies
that the following is code for a thread/process, while the
active keyword specifies that
the thread is started immediately when we start the program.
Variables declared outside the body of a
proctype are shared.
The keyword show before a
variable declaration will direct
SPIN to display the value as it changes.
Here, the two processes of deposit
and withdraw can interleave
arbitrarily. Regardless, with this very simple example,
we will always get the same result balance.
To run the code, we use SPIN. We'll describe how to use the
program xspin, which uses
a graphical interface.
More specifically, these instructions are for UNIX version 4.1.3.
The PC and Mac versions are identical, except for how to start
the program. For details, see the program's
README.
Ask your local system administrator where the program is installed
on your computer.
Aside:
spin.
It is more difficult to use interactively, but is appropriate
for use non-interactive use, such as with scripts.
For its options, see the manual pages for
spin
and the related
pan.
xspin is just a graphical front-end to
spin.
The underlying spin commands and output
are displayed at
the bottom of the main xspin window.
These can be ignored.
owlside:
xspin or
spin
from Rice University's Owlnet, first type
setenv PATH /home/comp607/bin:$PATH.
Within SPIN, you'll work with a Promela program. If you already have a Promela program saved, you can open it with the "File" menu's "Open" option. Alternatively, start SPIN with the Promela program's filename:
xspin filename.pml.pml.)
Either of these loads the Promela code into an editor window,
where it can be modified.
To create a new program, you can type into this window, or you can
copy and past it from another editor.
After loading or typing a program, it's always a good idea to check your typing. In the "Run" menu, use "Run Syntax Check". It will report and highlight the first syntax error, if any.
Before the first run, you need to specify some parameters, even if those are the defaults. From the "Run" menu, select the "Set Simulation Parameters" option. Change the "Display Mode" defaults, by making sure that the "MSC" and "Data Values" panels are each selected, as well as each feature under the "Data Values" panel. Use the "Start" button to start the run. This opens three new windows. From the "Simulation Output" window, you can choose to single-step the run or let it finish. For now, choose "Run". The body of this window displays each step taken. Throughout the run, the "Data Values" window displays the values of variables. The "Message Sequence Chart" displays changes to each process and variable. Each column represents a separate process or variable, and the vertical axis represents time. For now, the only changes to processes are when they stop.
Each process gets a unique process identifier number, starting with 0. These are displayed in the output of the various windows. Later, we will see how to access these IDs from within the Promela program.
For additional runs, from the "Run" menu of the main window, you can select "(Re)Run Simulation". However, each run will be identical, because it makes the same choices for interleaving. To vary runs, or traces, change the "Seed Value" in the simulation options. Also, be sure to try the "Interactive" simulation style, where you make each choice in the interleaving.
Aside:
xspin run opens several new windows,
without closing the old ones.
The easiest way to close the windows for a run is to use the "Cancel"
button in the "Simulation Output" window.
Aside:
Using spin from the command-line:
spin than xspin,
since you don't need to set
any of the simulation parameters:
prompt> spin filename.pml
- Definition 1: State
- A snapshot of the entire program. In Promela, this is an assignment to all the program's variables, both global and local, plus the program counter, or line number, of each process. In other languages, the state also includes information such as each process' stack contents.
- Definition 2: Trace
- A sequence — possibly infinite — of successive states, corresponding to one particular run of a program.
- See Also: state
Exercise 1
How many traces are possible for the
previous example's code?
Does each end with the same value for
bal?
Solution
|
Did you see each one using SPIN?
Each trace ends with bal
equal to 0, as expected.
Aside:
- SPIN adds an extra transition and state at the end of each process. You can think of the transition as the process exiting, but it doesn't change any of the variables' values. We omit this, except for one homework exercise.
-
Marking variables with
showintroduces transitions and states to display their changing values. We have omitted these. - Finally, as a minor presentational detail, SPIN doesn't report the start state of a trace like we do.
All the traces of a Promela program start at the same initial state. Any variables not explicitly initialized by the code are set to particular known values: zero, for numbers; and false, for booleans. (Compare to a C program, where two traces of the same program might each start from a different state.) And we've just seen that different traces may end in different states, or they may sometimes end in the same state while having taken different paths to reach that end state. Finally, if we look closely at all the possible traces, we see that often, several different traces might happen to pass through the same intermediate state. By combining the traces at these common states, we can depict the entire state space — a map of all the conceivable executions of our program!
- Definition 3: State space
- A directed graph with program states as nodes. The edges represent program statements, since they transform one state to another. There is exactly one state, the initial state, which represents the program state before execution begins.
- See Also: state, end state, valid end state
- Definition 4: Valid end state
- (Informal) Some states of our state space may be designated valid end states. These represent points where all of the threads have completed execution.
- See Also: deadlock, end state
Example 2
![]() |
The above is the state space of the Tiny First Program's code. The states are shown with the value of the only variable's value, plus the program point of each process. The edges are shown only as arrows, as the change in line numbers indicates which statement's and thread's execution each represents.
The possible traces are then all the different paths in the state space. State spaces are primarily helpful for understanding how to verify concurrent programs, and will be used mostly in the next section.
Aside:
xspin
by the "Run" menu's option to
"View Spin Automaton for each Proctype".
Also, the state numbering of this per-process form is reported
by SPIN's output of traces.
There is a close relation between the two forms, as
the whole system is intuitively some sort of
cross-product
of the individual processes.
We now modify the previous example. The two assignment statements have each been broken up into three statements. These three simulate a typical sequence of machine-level instructions in a load/store style architecture. This is a way to model that level of atomicity within Promela.
Example 3: A Race Condition
1 /* A variable shared between all processes. */
2 show int bal = 0;
3
4 active proctype deposit()
5 {
6 show int new_bal;
7
8 new_bal = bal;
9 new_bal++;
10 bal = new_bal;
11 }
12
13 active proctype withdraw()
14 {
15 show int new_bal;
16
17 new_bal = bal;
18 new_bal--;
19 bal = new_bal;
20 }
|
Here, each process has its own local variable named
new_bal.
Note that we can also have SPIN
show local variables in the
"Message Sequence Chart" window.
Run this code as described in the previous example.
Exercise 2
How many possible traces does this code allow? (Try it in SPIN!)
Solution
This code has 70 possible traces — many more than the previous version. Even with such a small program, it is too cumbersome to try to list them all. This is a fundamental reason why it is so hard for humans to debug a concurrent program, since it involves understanding all the possible ways threads can interact. Once we start using SPIN as a verifier, letting it enumerate these as a part of verification will be one of the benefits of automation. Later, we will verify this specific example.
Exercise 3
More importantly, what is the result of the computation,
i.e., the
value of bal at the end?
Solution
It can be 0, as expected, but it can also be 1 or -1! These new possibilities are the result of traces such as the following:
|
In particular, after one process has updated and printed the global variable, the other process can update it based upon the original un-updated value.
Exercise 4
Each of the previous two exercises could be very easily answered, if you were given the program's state space. Draw the state space.
Solution
![]() |
Whew! See how much larger this is than the state space of simpler version of this program.
Making the atomicity more fine-grained increases the possibility of concurrency problems. We will use this idea in many of the examples in this module, because we want to ensure that we can handle even the hard problems. The particular code sequence used in the previous example is also one of the shortest with a race condition.
Example 4: A Race Condition Variation
In Promela, initialization of a variable and assigning to a variable have subtly different semantics. This is illustrated by this variation on the previous example.
1 /* A variable shared between all processes. */
2 show int bal = 0;
3
4 active proctype deposit()
5 {
6 show int new_bal = bal;
7
8 new_bal++;
9 bal = new_bal;
10 }
11
12 active proctype withdraw()
13 {
14 show int new_bal = bal;
15
16 new_bal--;
17 bal = new_bal;
18 }
In Promela, initializing variables is part of starting a process.
Thus, all variables of all active processes are initialized
(defaulting to 0) before any other statements are executed.
Here, each copy of new_bal starts
with the value 0, so bal always
ends with either the value 1 or -1, but never 0.
![]() |
Example 5: A Race Condition Generalized
The previous examples modeled two people simultaneously updating the
same bank account, one making a deposit, and one a withdrawal.
What if we had even more simultaneous updates?
Let's focus only on deposits, so each is doing the same action.
Rather than duplicating code with multiple
proctype's (say,
deposit1,
deposit2, and
deposit3),
Promela allows us to
abstract that, as in the following:
1 /* Number of copies of the process to run. */
2 #define NUM_PROCS 3
3
4 /* A variable shared between all processes. */
5 show int z = 0;
6
7 active[NUM_PROCS] proctype increment()
8 {
9 show int new_z;
10
11 new_z = z;
12 new_z++;
13 z = new_z;
14 }
We've abstracted the variables' names, but otherwise,
increment is the same as the
previous deposit.
The first substantial change is that we are starting three processes,
rather than two, each with a copy of the same code.
The bracket notation in the
proctype declaration indicates
how many copies of that code are to be used.
The number of copies must be given by a constant,
and it is good practice to name that constant as shown.
Although vastly simplified, this is also the core of our
web-based flight reservation system example.
The shared variable z
would represent the number of booked seats.
Exercise 5
We can also ask questions like
“What is the probability that we end
with z having the value 3?”
How would you solve this?
Solution
Divide the number of traces meeting this condition by the total number of conceivable traces. 1
However, this assumes that each possible trace is equally likely — a dangerous assumption! In real life, with a particular operating system and hardware, some traces might be much more common than others. For example, it might be very unlikely (but possible) that two context switches would occur only a small number of instructions apart. So, a more realistic answer would be based upon a statistical analysis of repeated runs.
But an even more preferable answer is to say that this is a trick question! Rather than thinking of our concurrent system as being probabilistic, it is often better to stop short and merely call it non-deterministic. For example, if processes are distributed across the internet, then which trace occurs might depend on network traffic, which in turn might depend on such vagaries as the day's headlines, or how many internet routers are experimenting with a new algorithm, or whether somebody just stumbled over an ethernet cord somewhere. In practice, we can't come up with any convincing distribution on how jobs get scheduled. Moreover, sometimes we want to cover adversarial schedules (such as guarantees of how our web server protocol acts during denial-of-service attack).


























