Chapter 26 posed the problem: sequences we wish were atomic, shredded by interrupts and parallelism. The direct attack is the lock lock A variable holding one of two states β available (free) or acquired (held by exactly one thread, the owner) β with lock() not returning until the caller holds it, and unlock() freeing it. Programmers bracket critical sections with them, making the section execute as if atomic. defined in ch. 28 β open in glossary .
28.1 Locks: The Basic Idea
The canonical critical section:
balance = balance + 1;
To protect it, bracket it:
lock_t mutex; // some globally-allocated lock 'mutex'
...
lock(&mutex);
balance = balance + 1;
unlock(&mutex);
A lock is just a variable, holding one of two states: available (unlocked, free) or acquired (locked, held) β held by exactly one thread, the owner, presumably inside the critical section. (The type may hide more β the holderβs identity, a queue of waiters β but thatβs the implementationβs business.)
Semantics: lock() tries to acquire; if the lock is free, the caller
takes it and enters. If another thread holds it, the call does not
return while that remains true. unlock() frees it β and if
threads are stuck waiting, one of them will (eventually) notice or be
informed, acquire, and enter. The deeper point: threads are scheduled
by the OS, chaotically; a lock hands a little of that control back β
no more than one thread will ever be active in this code.
28.2 Pthread Locks
POSIX calls its lock a mutex mutex POSIX's lock object (pthread_mutex_t): lock() blocks until the caller holds it, unlock() releases. Must be INITIALIZED (static initializer or pthread_mutex_init) and its return codes CHECKED β the two classic omissions. trylock/timedlock variants exist; mostly avoid them. defined in ch. 27 β open in glossary (it provides mutual exclusion β one thread in, others excluded):
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
Pthread_mutex_lock(&lock); // wrapper; exits on failure
balance = balance + 1;
Pthread_mutex_unlock(&lock);
Note the lock is passed in: different locks can protect different data. One big lock for everything is coarse-grained; a lock per structure β fine-grained locking fine-grained locking Protecting different data with different locks so more threads can hold locks at once β versus one coarse-grained lock for everything. More concurrency, more care required. defined in ch. 28 β open in glossary β lets more threads run locked code at once.
28.3 Building A Lock
You now know how a lock behaves. But how does one build it?
The Crux: How To Build A Lock
How can we build an efficient lock? Efficient locks provide mutual exclusion at low cost, and might attain a few other properties. What hardware support is needed? What OS support?Answering takes both old friends: hardware (atomic instructions, arriving over the next sections) and the OS (sleeping support, arriving after that).
28.4 Evaluating Locks
First, the grading rubric:
| the question | |
|---|---|
| mutual exclusion | does it WORK β does it keep multiple threads out of the critical section? |
| fairness | does every contender get a fair shot once the lock frees β or can a thread STARVE, never obtaining it? |
| performance | what overhead does the lock add? |
28.5 Controlling Interrupts
The earliest solution, from the single-processor era:
void lock() {
DisableInterrupts();
}
void unlock() {
EnableInterrupts();
}
No interrupts, no interleaving β inside the section, your code runs as if atomic. One virtue, many vices:
| because⦠| |
|---|---|
| β simplicity | no interrupts β no interleaving β the section is effectively atomic |
| β trust | arbitrary programs get a privileged operation |
| β multiprocessors | disabling interrupts on one CPU stops nothing on the others |
| β lost interrupts | mask too long and events vanish |
| β slow | mask/unmask instructions execute slowly on modern CPUs |
Interrupt masking thus survives only in limited contexts: inside the OS itself, guarding kernel structures or avoiding messy interrupt-handling situations β where the trust issue disappears, since the OS always trusts itself with privileged operations anyhow. For everyone else, we need something better β and the hardware is about to provide it.
Check yourself
1.What states can a lock variable be in, and what do lock()/unlock() promise?
2.Why does the POSIX API pass a lock VARIABLE to lock/unlock rather than having one global lock?
3.Name the three axes for evaluating a lock β and the three performance cases worth separating.
4.lock() = DisableInterrupts() was the earliest mutual exclusion. List the fatal flaws.
5.Despite everything, interrupt masking still gets used. Where, and why is it OK there?