Multi-process algorithms

Semantics of multi-process algorithms and Atomicity

The following algorithm has one global variable x that is shared by N processes. Each process increments x in two steps. It first reads the value of x and stores x+1 in its local variable y. Then it writes the value of y to x. The goal is to check that, when the algorithm terminates, the value of x has been incremented by N. This is stated by the property Correctness.


CONSTANTS N (* Number of processes *)

(* PlusCal options (-termination) *)
(* --algorithm Atomicity { variables x = 0; process (P \in 1..N) variables y = 0; { l0: y := x+1; l1: x := y } } *)

VARIABLES x, pc, y vars == << x, pc, y >> ProcSet == (1..N) Init == (* Global variables *) /\ x = 0 (* Process P *) /\ y = [self \in 1..N |-> 0] /\ pc = [self \in ProcSet |-> "l0"] l0(self) == /\ pc[self] = "l0" /\ y' = [y EXCEPT ![self] = x+1] /\ pc' = [pc EXCEPT ![self] = "l1"] /\ x' = x l1(self) == /\ pc[self] = "l1" /\ x' = y[self] /\ pc' = [pc EXCEPT ![self] = "Done"] /\ y' = y P(self) == l0(self) \/ l1(self) Next == (\E self \in 1..N: P(self)) \/ (* Disjunct to prevent deadlock on termination *) ((\A self \in ProcSet: pc[self] = "Done") /\ UNCHANGED vars) Spec == /\ Init /\ [][Next]_vars /\ \A self \in 1..N : WF_vars(P(self)) Termination == <>(\A self \in ProcSet: pc[self] = "Done")

AllDone == \A self \in 1..N: pc[self] = "Done" Correctness == [](AllDone => x=N)
  1. In the translation of the algorithm above, how are pc and y defined? What is the variable self? What is the meaning of the expression y' = [y EXCEPT ![self] = x+1]?
  2. How is defined the semantics of a multi-process algorithm? Give the semantics of this algorithm as a transition system.
  3. Is incrementation of x atomic or not? Justify.
  4. The property Correctness is not satisfied by the algorithm above. Explain why it is incorrect using the counter-example provided by the TLA toolbox.
  5. How are atomic and non-atomic steps modeled in a transition system? How is it specified in PlusCal and TLA+?
  6. Modify the labeling of the algorithm so that the increment of x becomes atomic. Give the semantics of this algorithm as a transition system. Prove that this new algorithm is correct using the TLA toolbox.

Labels hence define which blocs of statements are executed in an atomic way. Labeling is not mandatory, in particular it is useless for sequential programs. However, labeling is a key to the modeling of concurrent and distributed algorithms where correctness heavily depends on which statements can be executed in an atomic way. PlusCal puts restrictions on labels in order to let the translation in TLA+ be as close as possible to the algorithm. These constraints are described in section 3.7 of the PlusCal user manual (available at ~herbrete/public/TLA/c-manual.pdf).

Adding -label to the PlusCal options turns on the automatic labeling of the algorithm by the translator. This is the default behavior in the case of a uniprocess algorithm. The default labeling consists in adding a minimal set of labels to guarantee the constraints mentioned above. It thus results in maximizing the size of the atomic blocs of statements.

Implementing atomic blocks with a semaphore

We consider again the algorithm above, and we assume that it is not possible to read and write the global variable x in a single atomic step. In such a situation semaphores can be used to make blocks of non-atomic statements atomic. This is known as the problem of mutual exclusion in concurrent algorithms. At any time, at most one process is granted access to a shared resource (here the global variable x). We use model-checking to design an algorithm that solves this problem. We start from a design that we prove incorrect. Then, we use the counter-example provided by the TLC model-checker to improve the design, until we reach an algorithm that matches the specification.


The first step in the design process is to write a formal specification of the expected algorithm.

  1. Write in natural language the properties that the algorithm above must satisfy in order to solve the mutual exclusion problem.
  2. Formalize these requirements as TLA+ specifications.

Unfair semaphore

A classical way to implement synchronizations (in particular mutual exclusion) is to use semaphores (see Wikipedia page). A semaphore s is a shared integer variable that can be manipulated using two operations: P(s) and V(s). The operation P(s) checks if the semaphore s has a value greater than zero, then it decreases the value of s. If the value of s is instead less or equal to 0, the calling process is blocked until s has a positive value. The operation V(s) increments s. When s gets back a positive value only one process waiting on operation P(s) is unblocked. The other processes waiting on operation P(s) remain blocked.

  1. The algorithm below shows how to use semaphores to ensure mutual exclusion in the previous algorithm. Implement macros P(s) and V(s) as specified above and initialize variable s properly.
  2. Check that the algorithm is correct for the specification using the TLC model-checker.

CONSTANTS N  (* Number of processes *)

(* PlusCal options (-sf) *)

--algorithm MutexSemaphore {
variables x = 0;
  variables s; (* Shared semaphore *)
macro P(sem) { ... }
macro V(sem) { ... }
process (P \in 1..N)
variables y = 0;
loop: while (TRUE) { nocs: either goto nocs (* Doing something else *) or skip; lock: P(s); l0: y := x+1; l1: x := y; unlock: V(s); } } } *) \* BEGIN TRANSLATION

(* Add the requirements as TLA+ specifications *)