Lecture 04: Mutual Exclusion II
Outline
- Recap of Lecture 03
- Critical Sections and Locks
- Interlude: Events and Timing
- The Peterson Lock
Last Time
- Considered shared backyard problem with Finn and Ru
- both dogs cannot be in backyard at same time
- communication via raised/lowered flags
- We learned
- achieving mutual exclusion and deadlock-freedom is subtle
- solution required an asymmetric protocol
- Our protocol gave preferential treatment to Finn
Previous Protocol
Will’s Protocol:
- Raise flag
- When Scott’s flag is lowered, let Finn out
- When Finn comes in, lower flag
Scott’s Protocol:
- Raise flag
- While Will’s flag is raised: (a) lower flag, (b) wait until Will’s flag is lowered (c) raise flag
- When Scott’s flag is up and Will’s is down, release Ru
- When Ru returns, lower flag
Crucial Insights
- Obtained mutual exclusion because of flag principle:
- both Scott and Will raise flags
- both look at other’s flag
- at least one sees other’s flag up
- at least one doesn’t release dog
- Obtained deadlock-freedom by deferment:
- if both dogs want to go out, Scott defers to Will
Protocol Gives Mutual Exclusion and Deadlock-freedom…
A Stronger Liveness Condition
Starvation-freedom If a dog wants to go out, eventually it will be able to go out.
Does Third Protocol give starvation freedom?
Sorry, Ruple
Third protocol is not starvation-free!
- Finn could go out, come in, go out, come in…
- Scott only looks at Will’s flag when it is up
- Ruple never goes out
Can we achieve starvation-freedom?
Mutual Exclusion Problem
- safety property (bad things don’t happen)
- liveness properties (good things eventually happen)
- deadlock-freedom
- starvation-freedom
Note: starvation-freedom \(\implies\) deadlock-freedom
Bringing it Back to Computers
Multiple threads/processors attempt to:
- call a method, execute block of code, read/write to a field, …
Want to ensure:
- only one thread/processor accesses resource at a time
- eventually one (or all) threads/processors should get access
In Java
Coming back to our Counter
:
public class Counter {
long count = 0;
public long getCount () { return count; }
public void increment () {
count++; // this line of code is *critical*
}
public void reset () { count = 0; }
}
Critical Sections
A critical section of code is a block of code that should be executed sequentially by one thread at a time:
- no concurrent executions
- no interleaving of statements with other threads
For example
public void increment () {
// start critical section
count++;
// end critical section
}
Protecting Critical Sections with Locks
The Lock
interface has two (for now) methods:
-
void lock()
: when method returns, thread acquires lock
- thread waits until lock is acquired
-
void unlock()
: when method returns, thread releases lock
- lock is available for another thread to acquire it
Locks and Mutual Exclusion
A Lock
should satisfy safety and liveness:
- safety
-
mutual exclusion at most one thread holds any lock at any given time
- liveness
-
deadlock-freedom if multiple threads try to concurrently acquire a lock, one will eventually acquire it
-
starvation-freedom if a thread tries to acquire lock, it will eventually succeed
Using Locks 1
Object instance has a Lock
member variable:
public class SomeClass {
// an instance of an object implementing Lock
private Lock lock = new SomeLockImplementation();
...
}
Using Locks 2
Surround critical section with a try/catch/finally block
lock.lock(); // lock acquired after this
try {
// critical section
} finally {
lock.unlock(); // lock released after this
}
The finally
block ensures that lock.unlock()
is called even if there is an exception or return statement in the critical section!
A Locked Counter
public class Counter {
long count = 0;
Lock lock = new SomeLockImplementation();
public long getCount () { return count; }
public void increment () {
lock.lock();
try {
count++;
} finally {
lock.unlock();
}
}
// should probably lock this too...
public void reset () { count = 0; }
}
Alright
- Now we know how to use locks
- But how can we implement one?
Interlude: Events and Timing
Convenient Assumptions I
- Executions of programs/protocols/algorithms consist of discrete events
- e.g., perform elementary arithmetic, logic, comparison
- read or write values; send or receive messages
- call or return from a method/function
Convenient Assumptions II
- Events for a single thread/process occur sequentially
- given any two distinct events, one precedes the other
Convenient Assumptions III
- Events for different threads/processes can occur concurrently
- caveat: what if multiple threads concurrently write to same location?
- only one value written
- treat written value as latest event
- Next week: formally define “linearizability”
Timing of Events
Wall clock time:
- Every event \(a\) has an associated time \(t_a\) at which the event occurs
- if \(a\) precedes \(b\), then \(t_a < t_b\).
- Often \(t_e\) is time an operation completes
- if process \(P_1\) writes to a register at time \(t_1\), then any process \(P_2\) reading the register at time \(t_2 \geq t_1\) will read what \(P_1\) wrote.
Ordering of Events
- If event $a$ precedes $b$ (i.e., $t_a < t_b$) write $a \to b$
- If $a_1 \to a_2$, can associate an interval \(I_A = (a_1, a_2)\)
- Say \(I_A = (a_1, a_2)\) precedes \(I_B = (b_1, b_2)\) if \(a_2 \to b_1\)
- similarly, \(I_A \to b \iff a_2 \to b\)
-
\[a \to I_B \iff a \to b_1\]
Are These Assumptions Justified?
NO!
The Real World
Things are not so nice!
- Even elementary operations do not happen instantaneously!
- Compilers, operating systems, and hardware make decisions that are out of our control!
- these choices often privilege performace over correctness
Try to Rely on Robust Assumptions
Still Though
We have to work very carefully to ensure that our assumptions are as close to reality as possible!
- Make reasonable, informed assumptions
- Write code such that the assumptions are most likely to be correct
When the assumptions are correct, our protocols are guaranteed to work
Going Forward
We will continue to make unjustified assumptions about how computers behave
- Assumptions will
- help us reason about protocol correctness
- help us appreciate how subtle these problems are, even in an idealized setting
- allow us to prove correctness of protocols in idealized models of computation
- We will see later
- how to make our computers come as close as possible to satisfying our assumptions
- turn theory into practice
Another Warning
Treat code for the next few lectures as “Java-like pseudo-code”
- follow Java syntax conventions
- may not be compilable Java
Yet Another Protocol
Previous protocol satisfied
- mutual exclusion
- deadlock-freedom
but not
We want more!
Symmetry Breaking Idea
Consider:
- Process \(A\) and \(B\) both write names in same field
- Processes repeatedly read from field
- Eventually, they will agree on name written in field
- That process is victim
- victim defers to other process
Peterson Lock (2 Threads)
Fields:
- flag (boolean) for each thread
- indicates intent to acquire lock
- value (int) shared between threads
- indicates identity of victim
Acquiring Peterson Lock
- Set my flag to be
true
- Set myself to be
victim
- While other’s flag is
true
and I am victim:
Releasing Peterson Lock
- Set my flag to
false