8.1 State Space Diagrams

A state space diagram is the primary tool for verifying correctness properties (mutual exclusion, deadlock-freedom, starvation-freedom) of concurrent protocols with a small number of threads and shared variables.

Mutual Exclusion (in terms of states)

A protocol satisfies mutual exclusion if no reachable state has both threads simultaneously in their critical section. Equivalently, the state does not appear in .

Deadlock

A state is a deadlock if it has no outgoing edges, i.e., no thread can make progress, yet at least one thread has not finished (is not in the non-critical section). A protocol is deadlock-free if no reachable state is a deadlock.

Starvation

A protocol suffers from starvation if there exists an infinite execution path (an infinite sequence of states in the diagram) along which some thread that is trying to enter its critical section never does so.
A protocol is starvation-free if every thread that attempts to enter the critical section eventually succeeds.

Each node of the state space diagram is a “node”:

System State

A state of a concurrent protocol is a tuple listing the current program location of each thread together with the current values of all shared variables. For two threads and with shared variables , a state has the form

where denotes that thread is about to execute line , and similarly for .

Example: from the exercise sheet 9

8.1.1 Reducing the State Space

In practice, the full state space is exponentially large. The standard reduction is:

  • Collapse all program locations that are observationally equivalent with respect to the shared-variable values relevant to the protocol merge into one abstract node.
  • Only retain transitions involving the pre-protocol, critical section, and post-protocol steps.

8.2 Locks

Atomic Register

A register supports operations and . It is atomic if every invocation takes effect at a single point in time satisfying:

  • lies between the start and end of ,
  • any two operations on the same register have distinct effect times,
  • returns the value written by the with the largest strictly before .

In Java: volatile primitives and AtomicInteger / AtomicIntegerArray give atomic registers.

Events and Precedence

Thread produces a sequence of events . The -th occurrence of event is written . We write if event occurs before event . Within a single thread, is a total order.

Interval and Concurrent Intervals

An interval is a pair of events with . For intervals and we write if (” precedes ”). If neither nor , the intervals are concurrent.

8.2.1 Two-Process Locks

We consider two processes and sharing volatile variables. Each attempt below reveals a different failure mode.

1st Try — Wrong Order of Flag and Wait

volatile boolean wantp = false, wantq = false;
 
P:  while (wantq);   // wait
    wantp = true;    // announce intent
    CS_P
    wantp = false;
 
Q:  while (wantp);
    wantq = true;
    CS_Q
    wantq = false;

Both threads can pass the while before either sets its own flag, reaching and simultaneously. Mutual exclusion is violated.

2nd Try — Announce Then Wait

volatile boolean wantp = false, wantq = false;
 
P:  wantp = true;    // announce first
    while (wantq);   // then wait
    CS_P
    wantp = false;

If both announce before either checks, both spin forever waiting for the other to retract. Deadlock.

3rd Try — Turn Variable Alone

volatile int turn = 1;
 
P:  while (turn != 1);   CS_P   turn = 2;
Q:  while (turn != 2);   CS_Q   turn = 1;

Mutual exclusion and deadlock-freedom hold, but if stalls outside its CS (allowed by our assumptions), can never re-enter once it sets turn = 2. Starvation.

8.2.2 Dekker’s Algorithm

Dekker combines tries 2 and 3: each process announces intent and yields via a turn variable only when there is a conflict.

volatile boolean wantp = false, wantq = false;
volatile int turn = 1;
 
P:
  wantp = true;
  while (wantq) {
      if (turn == 2) {       // Q has preference
          wantp = false;     // let Q proceed
          while (turn != 1); // and wait
          wantp = true;      // then try again
      }
  }
  CS_P
  turn = 2;
  wantp = false;

is symmetric with roles swapped. This is the first correct two-process mutual exclusion algorithm using only atomic registers.

8.2.3 Peterson’s Lock

A cleaner solution than Dekker’s, using a single victim variable for conflict resolution.

volatile boolean flag[2] = {false, false};
volatile int victim;
 
lock(me):
  flag[me] = true;            // "I am interested"
  victim   = me;              // "but you go first"
  while (flag[1-me] && victim == me);
 
unlock(me):
  flag[me] = false;

Key Idea: The victim variable ensures that if both processes try to enter simultaneously, only the one who isn’t the victim can proceed immediately.