018 Mutex2
018 Mutex2
Mutual Exclusion
2.1 Time
Reasoning about concurrent computation is mostly reasoning about time. Some-
times we want things to happen at the same time, and sometimes we want them
to happen at different times. We need to articulate complicated conditions in-
volving how multiple time intervals can overlap, or perhaps how they can’t. We
need a simple but unambiguous language to talk about events and durations
in time. Common-sense English is not sufficient, because it is too ambiguous
and imprecise. Instead, we will introduce a simple vocabulary and notation to
describe how concurrent threads behave in time.
In 1689, Isaac Newton stated “absolute, true and mathematical time, of itself
and from its own nature, flows equably without relation to anything external.”
We endorse his notion of time, if not his prose style. Threads share a common
time (though not necessarily a common clock). Recall that a thread is a state
machine, and its state transitions are called events. Events are instantaneous:
they occur at a single instant of time. Events are never simultaneous: distinct
events occur at distinct times. A thread A may produce a sequence of events
a0 , a1 , . . .. Threads typically contain loops, so a single program statement can
produce many events. It is convenient to denote the j-th occurrence of an event
0 This chapter is part of the Manuscript Multiprocessor Synchronization by Maurice
1
2 CHAPTER 2. MUTUAL EXCLUSION
class Counter {
private int value = 1; // counter starts at one
time. Assume, for the sake of the definition, that an execution of a program
involves the executing the critical section infinitely often, with other non-critical
operations taking place in between. Here are three properties a good mutual
exclusion protocol might satisfy.
Mutual Exclusion Critical sections of different threads do not overlap. For
k j j
threads A and B, and integers j and k, either CSA → CSB or CSB →
k
CSB .
No Deadlock If some thread wants in, some thread gets in. If thread A calls
acquire but never acquires the lock, then other threads must have com-
pleted an infinite number of critical sections.
No Lockout Every thread that wants in, eventually gets in. Every call to
acquire eventually returns.
Note that the no-lockout property implies the no-deadlock property.
The Mutual Exclusion property is clearly essential. The deadlock-free prop-
erty is important. It implies that the system never “freezes”. Individual threads
may be stuck forever (called starvation), but some thread makes progress. Note
that a program can still deadlock even if all the locks it uses are individually
deadlock free: for example, A and B may try to acquire two locks in different
orders.
The lockout-free property, while clearly desirable, is the least compelling of
the three. Later on, we will see “practical” mutual exclusion algorithms that
fail to be lockout-free. These algorithms are typically deployed in circumstances
where starvation is a theoretical possibility, but is unlikely to occur in practice.
Nevertheless, the ability to reason about starvation is a perquisite for under-
standing whether it is a realistic threat.
The lockout-free property is also weak in the sense that makes no guarantees
how long a thread will wait before it enters the critical section. Later on, we
will look at algorithms that place bounds on how long a thread can wait.
j
Proof: Suppose not. Then there exist integers j and k such that CSA 6→
k k j
CSB and CSB 6→ CSA . Consider each thread’s last execution of the acquire
method before entering its k-th (j-th) critical section.
Inspecting the code, we see that
Note that once f lag[B] is set to true it remains true. It follows that Equation
2.3 holds, since otherwise thread A could not have read f lag[B] as false.
Equation 2.4 follows from Equations 2.1, 2.3 and 2.2, and from the transitiv-
ity of precedence. It follows that writeA (f lag[A] = true) → readB (f lag[A] ==
f alse) without an intervening writeA (f lag[A] = f alse), a contradiction.
Why is the Lock1 class inadequate? It deadlocks if thread executions are
interleaved. If both writeA (f lag[A] = true) and writeB (f lag[B] = true) events
occur before readA (f lag[B]) and readB (f lag[A]) events, then both threads wait
forever. Why is the Lock1 class interesting? If one thread runs before the other,
no deadlock occurs, and all is well.
6 CHAPTER 2. MUTUAL EXCLUSION
Thread B must assign B to the victim field between events writeA (victim = A)
and readA (victim = B) (see Equation 2.5). Since this assignment is the last,
we have
Once the victim field is set to B, it does not change, so any subsequent read
will return B, contradicting Equation 2.6.
Why is the Lock2 class inadequate? It deadlocks if one thread runs com-
pletely before the other. Why is the Lock2 class interesting? If the threads
run concurrently, the acquire method succeeds. The Lock1 and Lock2 classes
complement one another: each succeeds under conditions that cause the other
to deadlock.
Assume, without loss of generality, that A was the last thread to write to
the victim field.
Equations 2.9, 2.10, and 2.11, and transitivity of → imply Equation 2.12.
It follows that writeB (f lag[B] = true) → readA (f lag[B] == f alse). This ob-
servation yields a contradiction because no other write to f lag[B] was performed
before the critical section executions.
Proof: Suppose not. Suppose (without loss of generality) that A runs forever
in the acquire method. It must be executing the while statement, waiting until
either flag[B] becomes false or victim is set to B.
What is B doing while A fails to make progress? Perhaps B is repeatedly
entering and leaving its critical section. If so, however, then B will set victim
to B as soon as it reenters the critical section. Once victim is set to B, it
will not change, and A must eventually return from the acquire method, a
contradiction.
So it must be that B is also be stuck in its call to the acquire method,
waiting until either flag[A] becomes false or victim is set to A. But victim
cannot be both A and B, a contradiction.
• At least one thread among all that want to enter level ` succeeds in doing
so.
• If more than one thread wants to enter level `, then at least one is blocked
from doing so.
2.4. N -THREAD SOLUTIONS 9
Inspecting the code, we see that B writes level[B] before it writes to victim[j]:
Inspecting the code, we see that A reads level[B] after it writes to victim[j]:
Proof: We argue by reverse induction on the levels. The base case, level n−1,
is trivial, because it contains at most one thread. For the induction hypothesis,
assume that every thread that reaches level j + 1 or higher eventually enters
(and leaves) its critical section.
Suppose A is stuck at level j. Eventually, by the induction hypothesis, there
will be no threads at higher levels. Once A sets level[A] to j, then any thread
at level j − 1 that subsequently reads level[A] is prevented from entering level
j. Eventually, no more threads enter level j from lower levels. All threads stuck
at level j are in the busy-waiting loop, and the values of the victim and level
fields no longer change.
We now argue by induction on the number of threads stuck at level j. For
the base case, if A is the only thread at level j or higher, then clearly it will
enter level j +1. For the induction hypothesis, assume that fewer than k threads
cannot be stuck at level j. Suppose threads A and B are stuck at level j. A
is stuck as long as it reads victim[j] = A, and B is stuck as long as it reads
victim[j] = B. The victim field is unchanging, and it cannot be equal to both
A and B, so one of the two threads will enter level j + 1, reducing the number
of stuck threads to k − 1, contradicting the induction hypothesis.
2.5 Fairness
The lockout-free property guarantees that every thread that calls acquire will
eventually enter the critical section, but it makes no guarantees about how long
it will take. Ideally, and very informally, if A calls acquire before B, then A
should enter the critical section before B. Such a guarantee is impossible to
provide with the tools at hand, and is stronger than we really need. Instead,
we split the acquire method into two intervals:
2.6. THE BAKERY ALGORITHM 11
In English: if thread A finishes its doorway before thread B starts its doorway,
then A can be “overtaken” at most r times by B. The strong form of fairness
known as first-come-first-served is equivalent to 0-bounded waiting.
It is easy to see that a thread’s labels are strictly increasing. Threads may have
the same label, but thread indexes break any ties when comparing pairs.
Proof: Some waiting thread A has the unique least (label[A], A) pair, and
that thread can return from the acquire method.
DA → DB
Proof: Suppose not. Let A and B be two threads concurrently in the critical
section. Let labelingA and labelingB be the respective sequences of acquiring a
new label by choosing one greater than all those read. Suppose (label[A], A) <
(label[B], B). When B entered, it must have seen that f lag[A] was false or that
label[A] > label[B].
But labels are strictly increasing, so B must have seen that f lag[A] was
false. It follows that
time t data structure will overflow when the number of seconds since 1 January
1970 exceeds 21 6. Sometimes, of course, counter overflow is a non-issue. Most
applications that use, say a 64-bit counter are unlikely to last long enough for
rollover to occur.
Let us focus on the role of the label values in the Bakery algorithm. Labels
act as timestamps: they establish an order among the contending threads. In-
formally, we need to ensure that if one thread takes a label after another, then
the latter has the larger label. Inspecting the code for the Bakery Algorithm
we see that a thread needs two abilities:
• to read the other threads’ timestamps (scan), and
• to assign itself a later timestamp (label ).
A Java interface to such a timestamping system appears in Figure 2.9.
Can we construct such an object? Ideally, since we are implementing mutual
exclusion (the timestamping system will be for example part of the doorway of
the Bakery Algorithm) any such algorithm should be wait-free. It turns out that
it is possible to implement such a wait-free concurrent timestamping system.
Unfortunately, the full construction is long and rather technical, so instead we
will focus on a simpler problem, a sequential timestamping system, in which we
assume that a thread can scan and label in a single atomic step. The principles
are essentially the same as in the concurrent case, but the details are much
simpler.
Think of the set of timestamps as nodes of a directed graph (called a prece-
dence graph). There is an edge from node a to node b if a is a later timestamp
than b. The timestamp order is irreflexive: there is no edge from any node a
to itself. It is also antisymmetric: if there is an edge from a to b, then there is
no edge from b to a. Notice that we do not require that the order be transitive:
an edge from a to b and from b to c does not necessarily mean there is an edge
from a to c.
Think of assigning a timestamp to a thread as placing that thread’s token
on a node. A thread performs a scan by locating the other threads’ tokens, and
it performs a label by moving its own token to a node a such that there is an
edge from a to every other thread’s node.
14 CHAPTER 2. MUTUAL EXCLUSION
Theorem 2.8.1 Any algorithm that solves deadlock free mutual exclusion must
use n distinct memory locations.
Lets understand how one would prove this fact for three threads, that is,
that three threads require at least three distinct memory fields to solve mutual
exclusion with no deadlock in the worst case.
Theorem 2.8.2 There is no algorithm that solves deadlock free mutual exclu-
sion for three threads using less than three distinct memory locations.
Proof: Assume by way of contradiction that there is such an algorithm for
processors A, B, and C, and it uses two variables. It should be clear that each
thread must write to some variable, otherwise we can first run that thread into
the critical section. Then run the other two threads and since the first thread
did not write there will be no trace in the two shared variables that he is in the
critical section and one of the other threads will also enter the critical section,
violating mutual exclusion. It follows that if the shared variables are single-
writer variables as in the Bakery algorithm, than it is immediate that three
separate variables are needed.
It remains to be shown that the same holds for multi-writer variables as such
as victim in Peterson’s algorithm. To do so, lets define the notion of a covering
state.
Definition A covering state is one in which there is at least one thread thread
poised to write to each shared variable, and the state of the shared variables is
consistent with all threads not being in the critical section or trying to enter
the critical section.
If we can bring threads A and B to a covering state where they respectively
cover the two variables RA and RB , then we can run thread C and it will have
to enter the critical section. Though it will have to write into RA or RB or both,
if we run both A and B since they were in a covering state their first step will be
to overwrite any information left by C in the shared variables and so they will
be running in a deadlock free mutual exclusion algorithm and one of them will
enter and join C in the critical section, a violation of mutual exclusion. This
scenario is depicted in Figure 2.12.
It thus remains to be shown how to manoeuver A and B into a covering
state. We will do so as follows. Consider an execution in which thread B runs
through the critical section three times. We have already shown that each time
it must write some variable, and lets look at the first variable it is about to write
in each round through the critical section. Since there are only two variables,
B must, during this execution, be twice poised to perform its first write to the
same variable. Lets call that RB .
Now, run B till its poised to write RB for the first time. Then run A until it
is about to write to variable RA for the first time. A must be on its way to enter
the critical section since B has not written. It must write RA at some point
before entering the critical section since otherwise, if it only writes to RB , we
can run B, obliterate any trace of A in RB , and then have B enter the critical
section together with RA , violating mutual exclusion.
2.9. GRANULARITY OF MUTUAL EXCLUSION 17
Now, on its way to writing RA , A could have left traces in RB . Here we use
the fact that we can run B, and since it was poised to write it will obliterate any
traces of A and enter the critical section. If we let B enter the critical section
twice more, it will as we have shown earlier have to reach a position where it
is poised to first write RB . This places A poised to write to RA and B poised
to write to RB , and the variables are consistent with no thread trying or in
the critical section, as required in a covering state. This scenario is depicted in
Figure 2.13.
In later chapters, we will see that modern machine architectures provide
specialized instructions for synchronization problems like mutual exclusion. We
will also see that making effective use of these instructions is far from trivial.
class Queue {
int head = 0; // next item to dequeue
int tail = 0; // next empty slot
Item[QSIZE] items;
class Queue {
If head and tail differ by QSIZE, then the queue is full, and if they are equal,
then the queue is empty. The enq method reads the head field into a local
variable, If the queue is full, the thread spins: it repeatedly tests the tail field
until it observes there is room in the items array. It then stores the item in the
array, and increments the tail field. The enqueue actually “takes effect” when
the tail field is incremented. The deq method works in a symmetric way.
Note that this implementation does not work if the queue is shared by more
than two threads, or if the threads change roles. Later on, we will examine ways
in which this example can (and cannot) be generalized.
We contrast these two implementations to emphasize the notion of granu-
larity of synchronization. The lock-based queue is an example of coarse-grained
synchronization: no matter how much native support for concurrency the hard-
ware provides, only one thread at a time can execute a method call. The lock-
free queue is an example of fine-grained synchronization: threads synchronize
at the level of individual machine instructions.
Why is this distinction important? There are two reasons. The first is fault-
tolerance. Recall that modern architectures are asynchronous: a thread can be
interrupted at any time for an arbitrary duration (because of cache misses, page
faults, descheduling, and so on). If a thread is interrupted while it holds a lock,
then all other threads that call that object’s methods will also be blocked. The
greater the hardware support for concurrency, the greater the wasted resources:
the unexpected delay of a single thread can potentially bring a massively parallel
multiprocessor to its knees.
By contrast, the lock-free queue does not present the same hazards. Threads
synchronize at the level of basic machine instructions (reading and updating
object fields). The hardware and operating system typically ensure that reading
or writing an object field is atomic: a thread interrupted while reading or writing
a field cannot block other threads attempting to read or write the same field.
The second reason concerns speedup. When we reason about the correctness
of a multi-threaded program, we do not need to consider the number of physical
processors supported by the underlying machine. A single-processor machine
can run a multithreaded program as well as an n-way symmetric multiprocessor.
Except for performance. Ideally, if we double the number of physical proces-
sors, we would like the running time of our programs to be cut in half. This never
happens. Realistically, most people who work in this area would be surprised
and delighted if, beyond a certain point, doubling the number of processors
provided any significant speedup.
To understand why such speedups are difficult, we turn our attention to
Amdahl’s Law. The key idea is that the extent to which we can speed up
a program a program is limited by how much of the program is inherently
sequential. The degree to which a program is inherently sequential depends on
its granularity of synchronization.
Define the speedup S of a program to be the ratio between its running time
(measured by a wall clock) on a single-processor machine, and on an n-way
multiprocessor. Let c be the fraction of the program that can be executed in
parallel, without synchronization or waiting. If we assume that the sequential
2.10. CHAPTER NOTES 21
program takes time 1, then the sequential part of the program will take time
1 − c, and the concurrent part will take time c/n. Here is the speedup S for an
n-way multiprocessor:
1
S=
1 − c + nc
For example, if a program spends 20% if its time in critical sections, and is
deployed on a 10-way multiprocessor, then Amdahl’s Law implies a maximum
speedup of
1
3.58 = 0.8
1 − 0.8 + 10.0
If we cut the synchronization granularity to 10%, then we have a speedup of
1
5.26 = 0.9
1 − 0.9 + 10.0
2.11 Exercises
1. Programmers at the Flaky Computer Corporation designed the following
protocol for n-process mutual exclusion with deadlock-freedom.
The tree-lock’s release method for the tree-lock unlocks each of the two-
thread Peterson locks that thread has acquired, from the root back to its
leaf.
Either sketch a proof that this tree-lock satisfies mutual exclusion, or given
an execution where it does not.
2.12 Bibliography
• L. Lamport, A New Solution of Dijkstra’s Concurrent Programming Prob-
lem, Communications of the ACM, 17 (8), 453-455, 1974.
• G. L. Peterson, Myths About the Mutual Exclusion Problem Information
Processing Letters, 12(3), pages 115-116, 1981.
• A. Israeli and M. Li. Bounded timestamps. Distributed Computing,
6(4):205-209, 1993.
24 CHAPTER 2. MUTUAL EXCLUSION