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.

    EXTENDS Naturals, TLC

    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
      }

    }
    *)

    \* BEGIN TRANSLATION
    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")

    \* END TRANSLATION

    AllDone == \A self \in ProcSet: 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. The property Correctness is not satisfied by the algorithm above. Explain why it is incorrect using the counter-example provided by the TLA toolbox.
  4. Is incrementation of x atomic or not? Justify.
  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 a program as above where a process needs to read and write a variable in an atomic step, in a setting where there is only non-atomic read/write operations. 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 (in the previous example, the global variable x). We use model-checking to design an algorithm that solves this problem.

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. What are the atomic propositions needed to express these properties? Formalize these specifications in LTL, and then as TLA+ specifications.

Modeling a 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 value of s is greater than zero, and decrements s, in a single atomic step. It is blocking if the value of s is smaller-than or equal-to zero. The operation V(s) increments s. When s gets back a positive value one process waiting on operation P(s) is unblocked (if any). The other processes waiting on operation P(s) remain blocked.

  1. The algorithm below shows how to use semaphores to ensure mutual exclusion (where the access to the shared resource has been abstracted away as the cs label and instruction skip). 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.
      EXTENDS Naturals, TLC

      CONSTANTS N  (* Number of processes *)

      (* PlusCal options (-sf) *)

      (*
      --algorithm Semaphore {
        variables s = ...;      (* Shared semaphore *)

        macro P(sem) { ... }
        macro V(sem) { ... }

        process (P \in 1..N)
        {
          loop:  while (TRUE) {
            lock:    P(s);
            cs:      skip;              (* In the critical section *)
            unlock:  V(s)
          }
        }

      }
      *)

      \* BEGIN TRANSLATION
      \* END TRANSLATION