Prev: introduction Next: concurrent-objects
Reasoning about concurrent computation is mostly reasoning about time. Sometimes we want things to happen simultaneously, and sometimes at different times.
Threads share a common time, and are state machines that transition through events.
We say that if a precedes b, this is written as
a -> b
, and applies a total order on events.
Mutual exclusion is used to guard critical sections, a block of code that can only be executed by one thread at a time.
public class Counter {
private long value;
private Lock lock; // for the critical section
public long getAndIncrement() {
.lock();
locktry {
long temp = value;
= temp + 1;
value return temp;
} finally {
.unlock();
lock}
}
}
Every lock should satisfy mutual exclusion, deadlock freedom, and freedom from starvation.
To solve the LockOne algorithm, a thread sets its flag to true and waits until the other thread’s flag is false. When that’s done, it finally returns.
class LockOne implements Lock {
private boolean[] flag = new boolean[2];
// thread-local index, 0 or 1
public void lock() {
int i = ThreadID.get();
int j = 1 - i;
[i] = true;
flagwhile (flag[j]) {} // wait until flag[j] == false
}
public void unlock() {
int i = ThreadID.get();
[i] = false;
flag}
}
Unfortunately, this algorithm can deadlock if thread executions are interleaved.
This algorithm supports mutual exclusion, but it gets stuck unless the threads run concurrently, the opposite of LockOne.
class LockTwo implements Lock {
private int victim;
public void lock() {
int i = ThreadID.get();
= i; // let the other go first
victim while (victim == i) {} // wait
}
public void unlock() {}
}
The Peterson Lock supports a starvation-free lock for two-thread mutual exclusion.
class Peterson implements Lock {
// thread-local index, 0 or 1
private boolean[] flag = new boolean[2];
private int victim;
public void lock() {
int i = ThreadID.get();
int j = 1 - i;
[i] = true; // I’m interested
flag= i; // you go first
victim while (flag[j] && victim == i) {} // wait
}
public void unlock() {
int i = ThreadID.get();
[i] = false; // I’m not interested
flag}
}
Even though the Peterson Lock is deadlock-free, if multiple peterson locks are used, livelock can ensue, in which two threads prevent each other from making progress.
The filter lock generalizes the Peterson lock to work for n > 2.
It creates n - 1 waiting rooms
, called levels.
class Filter implements Lock {
int[] level;
int[] victim;
public Filter(int n) {
= new int[n];
level = new int[n]; // use 1..n-1
victim for (int i = 0; i < n; i++) {
[i] = 0;
level}
}
public void lock() {
int me = ThreadID.get();
for (int i = 1; i < n; i++) { // attempt to enter level i
[me] = i;
level[i] = me;
victim// spin while conflicts exist
while ((∃k != me) (level[k] >= i && victim[i] == me)) {};
}
}
public void unlock() {
int me = ThreadID.get();
[me] = 0;
level}
}
Although the filter lock is starvation-free, a thread might be overtaken arbitrarily many times by another thread, because accesses to the critical section are not first-come-first-served.
To make this happen, we can split the lock()
method into
a doorway and a waiting section, where the doorway completes in a
bounded number of steps, before entering the waiting area.
class Bakery implements Lock {
boolean[] flag;
Label[] label;
public Bakery (int n) {
= new boolean[n];
flag = new Label[n];
label for (int i = 0; i < n; i++) {
[i] = false; label[i] = 0;
flag}
}
public void lock() {
int i = ThreadID.get();
[i] = true;
flag[i] = max(label[0], ...,label[n-1]) + 1;
labelwhile ((∃k != i)(flag[k] && (label[k],k) << (label[i],i))) {};
}
public void unlock() {
[ThreadID.get()] = false;
flag}
}
Lamport’s bakery algorithm guarantees the
first-come-first-served
property, while fulfilling mutual
exclusion, being deadlock-free, and starvation free.
AtomicIntegerArray ticket = new AtomicIntegerArray(threads); // ticket for threads in line, n - number of threads
// Java initializes each element of 'ticket' to 0
AtomicIntegerArray entering = new AtomicIntegerArray(threads); // 1 when thread entering in line
// Java initializes each element of 'entering' to 0
public void lock(int pid) {
// thread ID
.set(pid, 1);
enteringint max = 0;
for (int i = 0; i < threads; i++) {
int current = ticket.get(i);
if (current > max) {
= current;
max }
}
.set(pid, 1 + max);
ticket.set(pid, 0);
enteringfor (int i = 0; i < ticket.length(); ++i) {
if (i != pid) {
while (entering.get(i) == 1) { Thread.yield(); } // wait while other thread picks a ticket
while (ticket.get(i) != 0 && ( ticket.get(i) < ticket.get(pid) ||
(ticket.get(i) == ticket.get(pid) && i < pid))) {
Thread.yield();
}
}
}
// The critical section goes here...
}
public void unlock(int pid) {
.set(pid, 0);
ticket}
Unfortunately, the bakery algorithm requires that timestamps never overflow, and are totally ordered. Of course, that’s not feasible, since a counter might overflow a 64-bit int, for example.
Prev: introduction Next: concurrent-objects