Critical section Created 2026-10-14 Updated 2026-10-25
A region whose accesses must obey a shared synchronization protocol. For the lost update in mutual exclusion, the critical section includes the read and the local addition as well as the write. Choosing its boundary means identifying the complete operation whose intermediate state must be protected. Every conflicting access must participate in a compatible protocol; merely declaring a mutex does not protect accesses that bypass it.
Deadlock 2026-10-14
A group of participants cannot proceed because each waits for a resource whose release depends on another participant in the group. Consider two initially free mutex objects A and B. T1 acquires A then tries B; T2 acquires B then tries A. If each obtains its first mutex, both wait permanently for the other, with release reachable only after the second acquisition. Each mutex still enforces mutual exclusion.[ref]
Figure 1.
Reachable states for opposite mutex acquisition orders and for one shared lock ordering
. Details.
Each software thread performs four indivisible operations: acquire its first mutex, acquire its second, release the second, release the first. An unavailable acquisition blocks; neither participant releases early. A state records each participant's completed-operation count and both owners. Exhaustive exploration finds 19 reachable states with opposite orders, including exactly one nonterminal state with no enabled operation: both counts are , T1 owns A, and T2 owns B. Giving both participants the order A then B leaves 16 reachable states and no such blocked state. Unshown grid positions are unreachable. These are tiny sequential consistency models, without timeouts, cancellation, recursive acquisition or other resources.
Happens-before 2026-10-14
An ordering binary relation used by a memory model to constrain observation of shared data. For the mutex example, T1's write precedes its release, that release synchronizes with T2's successful acquisition, and that acquisition precedes T2's read. Together these steps order the write before the read. Wall-clock timing alone does not establish happens-before.[ref]
Lock ordering 2026-10-14
A shared order governing acquisition of distinct mutex objects. To prevent circular waiting, choose a strict total order and acquire only a higher-ranked mutex while retaining lower-ranked ones. In the two-object deadlock example, every participant acquires A before B. An outline of the argument is that ranks would strictly increase around any alleged waiting cycle, yet return to the starting rank, a contradiction. The rule must include acquisitions hidden inside called routines. It excludes this kind of deadlock, not arbitrary waiting on other resources or failure to schedule an owner.[ref]
Mutex 2026-10-14
An object with acquire and release operations that provides exclusive ownership to a participating software thread. Several source-code regions can acquire the same mutex; two different mutex objects do not exclude each other's users. The caller should enter its critical section only after successful acquisition and release ownership on every exit path. A mutex may make a waiting software thread sleep or use a spinlock; the interface and the waiting mechanism are distinct.[ref]
Mutual exclusion Created 2026-10-14 Updated 2026-10-25
Mutual exclusion allows at most one participating software thread at a time to execute a protected critical section. A mutex is an object used to enforce that rule; the critical section is the region of the program that needs the rule. Several different critical sections can use the same mutex to protect the same shared data. The benefit is reasoning about a complete update without another cooperating software thread observing or changing its intermediate state.[ref]
For a small lost update, shared x initially equals . Each of two software threads, T1 and T2, uses its own local r and performs these three steps:
r = read(x)
r = r + 1
write(x, r)
Assume sequential consistency: each step is indivisible, each software thread keeps the displayed order, and a read sees the latest preceding write to x. In the thread interleaving T1 read, T2 read, T1 add, T2 add, T1 write, T2 write, both local values become and both writes store . Final instead of : a lost update despite two correctly executed local additions. Separate indivisible reads and writes are insufficient; the complete read, modify and write must be protected.
Figure 1.
A lost update and its repair by mutual exclusion around the complete increment
. Details.
The binomial coefficient counts the thread interleavings preserving the two three-step orders: choose T1's three positions, with the remaining positions belonging to T2. Exhaustive enumeration gives final in 18 orders and in the two serial orders. These counts are not probabilities: no random scheduler has been specified. With the same mutex acquired before reading and released after writing, only the two complete increment orders remain, and both give . Alternatively, an atomic read-modify-write increment updates x in one indivisible step. Protecting only the write still allows both software threads to read beforehand.
Mutual exclusion is a safety property: two participants must never be inside the protected critical section together. Eventual entry for every persistent requester is a liveness property, expressed here as starvation freedom. A protocol that never admits anyone satisfies the first requirement while failing the second. Nor does a mutex alone promise which waiting software thread enters next, or that a producer runs before a consumer. Such application ordering needs an additional synchronization protocol.[ref]
The sequential consistency assumptions delimit this calculation. In C++, unsynchronized conflicting accesses to an ordinary shared object can be a data race, which makes behavior undefined; the 20 schedules are not a prediction for that program. An atomic operation with relaxed ordering also supplies no acquire-release synchronization for other data. A real mutex must supply both exclusive entry and the required happens-before ordering between its users.[ref]
O1 supplies mutual exclusion over each complete operation. A reader sees the protected state from a serial order of readers and writers; a writer can safely mutate the existing object while holding the common mutex. If an update reads the old state and modifies it, the entire read-modify-write can be protected against a lost update.
O2 has different semantics. Assume an indivisible aligned access to p, correct publication ordering, and no later mutation of a published version. A reader chooses one version when it loads p; the subsequent load instructions use the same saved pointer, so an overlapping writer need not block that reader. The reader may continue observing the old complete version after the writer has published the new one. A sufficiently specified snapshot read can still be linearizable, with the load of p as its linearization point: a read overlapping a write is allowed to come before that write in the abstract order. Thus reading an old version during overlap does not by itself prove failure of linearizability.
Multiple writers can each construct a valid replacement and race to publish it. If the operation is simply “replace the whole object”, atomic pointer stores give a last-publication order. If each writer instead computes a change from the previously published state, O2 alone does not preserve both changes. Two writers can both read a counter value zero, independently build a version containing one, and publish their respective versions; the final counter is one. A writer mutex or compare-and-swap with retry is needed when the required operation must avoid that lost update. Memory barriers alone do not provide this exclusion.
Releasing the old object's computer memory immediately after publishing a replacement is unsafe. For example:
reader: q := LOAD [p]          // gets the old object
writer: STORE [p], new_object
writer: free(old_object)
reader: LOAD [q + field]       // accesses freed storage
The final operation is a use-after-free: it may crash, see allocator metadata, or read a different object that reuses the same memory address. An atomic update of p does not update the reader's already copied pointer. Freeing the new object immediately would of course also invalidate p itself.
Publication and reclamation are separate: publish a fully initialized version, and retain every old version until all readers that might hold it have finished. A read-copy-update grace period or a suitable hazard pointer protocol can provide that lifetime guarantee; naive reference-count acquisition after loading an unprotected pointer can itself race with freeing. Correct memory barriers in part (iii) ensure visibility, not object lifetime.
With O1, even readers must acquire and release the same mutex. Acquiring it normally uses an atomic read-modify-write on the lock's cache line; releasing writes that cache line again. As different CPU cores repeatedly read the data, ownership of the lock's cache line moves among them, causing cache coherence traffic and serializing the protected operations. If the lock shares a cache line with otherwise stable data, false sharing can also invalidate useful reader data. The actual data need not change for this lock traffic to occur.
With O2 and correctly ordered publication, readers perform only load instructions: copy p once into a processor register, then read that version of the data. Multiple CPU cores can keep read-only shared copies of its cache lines. No per-reader exclusive ownership transfer is needed. During an update, a writer fills newly allocated computer memory privately; the old version's cache lines remain unchanged for readers already using it. Publishing the new pointer writes p and invalidates cached copies of that pointer's cache line, but this occurs only on updates.
For many reads and rare updates, O2 avoids lock-line contention and permits concurrent readers, so it can be substantially faster. The costs are heap allocation, construction or copying of a whole replacement, first accesses to its newly populated cache lines, and deferred memory reclamation. Frequent updates or very large replacements can reverse the comparison. O1 can modify a small field in place and may then save copying and computer memory. These are workload-dependent comparisons, not a claim that removing every mutex always accelerates an operation.
A counting semaphore holds a nonnegative number of permits. down waits until a permit is available and consumes one; up returns a permit and can release a waiting caller. A binary semaphore restricts the number available to zero or one. Unlike a mutex, a semaphore need not be released by the same software thread that acquired it; this permits producer-to-consumer signalling.
Interpret maxCount as the initial number of permits and, for this bounded implementation, the maximum permitted number of unmatched releases. An up beyond that capacity is a programming error, rather than a blocking operation. Clients obey this release discipline. If an unbounded counting semaphore is desired instead, remove the upper-bound requirement and use a sufficiently wide counter; a capacity-one instance is binary only under the corresponding balanced-use discipline.
class CountingSemaphore(maxCount: Int) extends Semaphore {
  require(maxCount > 0)
  private var count = maxCount
  def down: Unit = synchronized {
    while (count == 0) wait()
    count -= 1
  }
  def up: Unit = synchronized {
    require(count < maxCount, "unmatched semaphore release")
    count += 1
    notify()
  }
}
All accesses to count occur under the same monitor. wait atomically releases that monitor while sleeping, then reacquires it before returning. The while is needed because of spurious wakeups and because another caller can consume the returned permit before the notified caller reacquires the monitor. Only down callers wait here, and they all wait on the same predicate; hence waking one with notify is sufficient. A later up notifies another waiter if necessary. This use of notify will not be safe for the mixed predicates in the next part.[ref]
A binary semaphore initially available is new CountingSemaphore(1). To initialize it unavailable, call its down once during construction, before publishing it to other software threads. Its state obeys
This implementation provides no first-come-first-served guarantee: a new caller can acquire the monitor before a notified waiter. The following examples assume ordinary successful execution without cancellation or interruption of a partially completed buffer operation.
Give the two waiting predicates different semaphores. empty has one permit initially; full has zero. A writer takes the empty permit before touching the slot and transfers permission to a reader by releasing full. A reader does the reverse.
class SemaphoreMemory[T] extends Memory[T] {
  private val empty: Semaphore = new CountingSemaphore(1)
  private val full: Semaphore = {
    val s = new CountingSemaphore(1)
    s.down
    s
  }
  private var slot: Option[T] = None

  def write(msg: T): Unit = {
    empty.down
    slot = Some(msg)
    full.up
  }
  def read: T = {
    full.down
    val result = slot.get
    slot = None
    empty.up
    result
  }
}
For a single slot, a separate mutex around slot is unnecessary. There is a unique circulating permission: either an empty permit is available, a writer holds it, a full permit is available, or a reader holds it. During the transition the sum of available permits can be zero; it is never necessary to assert that it is always one. In particular, the next writer cannot start until the preceding reader has finished reading, cleared the slot and released empty.
The monitor operations inside each semaphore also establish the required happens-before relation: the write precedes full.up, and a successful subsequent full.down makes it visible to the reader. Likewise, the reader's clearing precedes the next writer's successful empty.down. Slot accesses therefore need no unsynchronized visibility assumption. The invariant is
Only writers wait on empty, and only readers wait on full. With the single-notification semaphore implementation in part (a), a state change wakes at most one waiter of the appropriate kind, avoiding the thundering herd of part (i). This is an efficiency improvement, not yet a first-come-first-served guarantee.
If one or more readers arrive before the first writer, they block on the initially unavailable full semaphore. Their waits release its monitor, so they do not prevent a writer from obtaining the independent empty permit. The first write releases full and permits exactly one reader to take that value. Remaining readers wait for later writes; they never see uninitialized storage.
Pinned buffer 2026-10-14
A buffered device block whose identity is retained for unfinished work. Reference counting can represent both client references and additional pins. Dropping a client reference does not release an outstanding journal pin; dropping a pin does not remove somebody else's reference. In the buffer cache state model, counts all such retained references, so prevents buffer cache eviction. This count keeps the structure alive; a mutex separately controls who can inspect or change its contents. Waiting callers also need lifetime protection.
Semaphore (programming) 2026-10-31
A permit-based synchronization object. A down operation waits until it can consume a permit; an up operation supplies one. Unlike ownership-based mutex use, signalling can pass between different software threads.
Starvation freedom 2026-10-14
For acquisition of a mutex, the guarantee that every persistent requester eventually succeeds, under specified scheduling assumptions and with owners eventually releasing. Some software threads repeatedly succeeding while another waits forever violates starvation freedom without violating mutual exclusion. A finite bound on the wait, and service in arrival order, are additional requirements.
Thread interleaving 2026-10-14
An ordered execution obtained by mixing steps of several software threads while retaining each one's own step order. In the sequential consistency model of mutual exclusion, an indivisible step finishes before the next begins. Selecting the step boundaries is part of the model: an atomic read-modify-write increment has one boundary, whereas a separate read, local addition and write have three. Enumerating all such orders checks this specified model, not an arbitrary memory model or a real mutex implementation.
xv6 buffer cache 2026-10-14
The teaching implementation's bget checks device and block number under a shared spinlock, increments refcnt before releasing that lock, and then takes the buffer's content mutex. On a cache miss it reserves an unused structure and clears valid; bread fills it only if necessary. This makes lookup and reservation share one synchronization boundary. brelse releases the content mutex and decrements the reference count. Unlike the stipulated trace policy, it moves the structure to the recent end only when that decrement reaches zero. [ref]
This implementation has valid and refcnt but no generic dirty flag. Its file-system journaling code records modified blocks with log_write, pins each newly recorded block, copies cached contents to the journal during commit, and unpins after installing the committed contents at their final locations. Thus journal pins keep modifications resident after callers release them. Its eviction loop does not perform the generic dirty-victim buffer cache writeback of our model. [ref][ref]