Mutex Lock
A mutex (mutual-exclusion lock) is a synchronization primitive used solely to enforce mutual exclusion on a critical section.
Mutex vs binary semaphore
A binary semaphore has the same fields but does not track the owner. A mutex knows who holds it, so only the owner can release.
Typical fields (from CS343):
class MutexLock {
bool avail; // resource available?
Task * owner; // lock owner (nullptr if free)
queue<Task> blocked; // tasks waiting to acquire
SpinLock lock; // internal spin lock
};availalone is not enough, the blocked queue can be empty while the CS is still occupied, soavailexists independentlyownerletsreleaseerror-check and supports multiple acquisition (recursive re-entry, see uOwnerLock)- Internal
SpinLockserializes access to the three fields above; held for a short window around enqueue/dequeue
The yieldNoSchedule(lock) trick
Blocking must be atomic with spin-lock release, or there’s a race: blocker releases lock ⇒ preempted ⇒ unblocker enters, sees blocker in the queue, moves them to the ready queue ⇒ blocker is now on both the blocked queue and the ready queue. Fix: runtime cooperation.
// add self to blocked list
yieldNoSchedule( lock ); // runtime: yield slice, do NOT put on ready queue,
// then release lock on behalf of the taskPassing the spin lock into the runtime makes “release lock + yield without rescheduling” a single uninterruptible step. Alternative is park/unpark with a private per-task binary semaphore (see Solution 6 of Reader-Writers).
Barging: three regimes
- Plain (barging allowed), released task busy-waits in
while (! avail); incoming bargers can overtake, unbounded starvation possible - Barging avoidance, releaser holds
availuntil the unblocked task runs; bargers enter the protocol but block. Released task usesifnotwhile. Bounded overtaking, but a spinlock-unfair runtime still lets infinite bargers block the spinlock acquire - Barging prevention, releaser holds the spinlock itself (does not call
release) until the unblocked task runs. Critical section is no longer bracketed by the spinlock, the lock is conceptually passed via baton passing. The unblocked task wakes already owning the spinlock
// barging prevention (owner-less variant)
void acquire() {
lock.acquire();
if ( blocked.empty() ) { // no one waiting ?
// add self as owner at front
} else {
// add self to tail of blocked list
yieldNoSchedule( lock ); // block with lock STILL held by runtime
}
lock.release();
}
void release() {
lock.acquire();
if ( ! blocked.empty() ) {
// remove next task; do NOT lock.release()
// baton (spinlock ownership) is passed to the unblocked task
} else {
lock.release(); // no waiters, normal release
}
}In Rust, the standard pattern is to wrap a Mutex<T> in an Arc so multiple threads can share protected mutable state. Deadlock is still possible even with automatic unlock: thread 1 taking A then B while thread 2 takes B then A will still hang.