Condition Variables, Semaphores and Deadlock

Locks keep threads out of each other's way, but threads also need to wait for each other. This material shows how condition variables and semaphores make a thread sleep until data is ready, a slot is free or another thread has finished, and uses them to solve the producer–consumer and readers–writers problems. It then turns to what goes wrong in real concurrent code: atomicity and order violations, and deadlock, with ways to prevent, avoid and detect it. It ends with event-based concurrency, in which a single thread runs an event loop instead of using many threads. Examples use C with POSIX threads, traced step by step.

IntermediateLesson 6 of 9 in Operating Systems Course

What you will learn

  1. Make a thread sleep until a condition on shared data becomes true, using a condition variable together with a lock and a state variable, and use this pattern to build a correct bounded buffer.
    • Wait for a condition instead of spinning
      Explain why a lock alone cannot make a thread wait for an event, and use wait and signal with a lock and a state variable so that no wakeup is lost.
    • Recheck conditions and choose signal or broadcast
      Apply Mesa semantics by rechecking the condition in a while loop, decide between signal and broadcast, and recognize the same rules in monitors and Java.
    • Solve the producer–consumer problem with condition variables
      Build a bounded buffer shared by producer and consumer threads, trace why naive versions fail, and fix them with while loops and two condition variables.
  2. Use the semaphore, a single primitive, as a lock, as a signal between threads and as a counter of resources, and apply it to the bounded buffer and to a readers–writers lock.
    • Define a semaphore and use it as a lock
      Describe how a semaphore's value changes under wait and post, choose its initial value, and use a binary semaphore for mutual exclusion.
    • Order threads and limit concurrency with semaphores
      Use a semaphore that starts at zero to make one thread wait for another, and a counting semaphore to cap how many threads use a resource at once.
    • Build a bounded buffer with semaphores
      Solve producer–consumer with two counting semaphores and a mutex, and explain why the order in which they are acquired decides between a working buffer and a deadlock.
    • Share data with a readers–writers lock
      Allow many readers or one writer at a time, build such a lock from semaphores, and explain its starvation problem and costs.
  3. Identify atomicity violations and order violations, the two patterns behind almost all non-deadlock concurrency bugs found in a study of real software, and fix each with the right tool.
    • Find and fix atomicity violations
      Recognize code that assumes a group of accesses runs without interruption, show the interleaving that breaks it, and protect the whole group with one lock.
    • Find and fix order violations
      Recognize code that assumes one access happens before another, and enforce that order with a condition variable, a semaphore or join instead of timing.
  4. Explain how deadlock arises from four conditions, prevent it by designing out one of them, and describe how systems avoid deadlock with advance knowledge or detect and recover from it.
    • Explain how deadlock arises
      Define deadlock, show the interleaving that causes it, name the four necessary conditions, and use a wait-for graph to decide whether threads are deadlocked.
    • Prevent deadlock by breaking a condition
      Design code so that one of the four conditions can never hold: order lock acquisition, take all locks at once, back off with trylock, or avoid locks with atomic instructions.
    • Avoid deadlock with scheduling and the banker's algorithm
      Use advance knowledge of what threads may request to grant only requests that keep the system in a safe state, working through the banker's algorithm with real numbers.
    • Detect deadlock and recover
      Let deadlocks happen, find them by searching for cycles, recover by ending or rolling back a thread, and weigh this against prevention, avoidance and doing nothing.
  5. Structure a program around an event loop that reacts to ready input and output, avoid blocking with asynchronous I/O and saved state, and weigh events against threads.
    • Run an event loop with select or poll
      Write a single-threaded event loop that asks the operating system which descriptors are ready, handles each event to completion, and never blocks.
    • Avoid blocking with asynchronous I/O and saved state
      Issue I/O without waiting for it, find out when it has completed, and carry the state a handler needs across that gap.
    • Choose between events and threads
      Compare event-based and thread-based designs on control, memory, multicore use and maintainability, and recognize the bugs that remain in event code.

Condition Variables and Semaphores study guide

5 chapters

1

Wait for shared state to change

A thread often needs more than exclusive access: it needs another thread to finish work or make data available. Condition variables let it sleep during that wait. Their correctness comes from a shared state variable, a mutex, and a careful handoff between checking and sleeping. Start with a parent waiting for a child, then use the same pattern to coordinate producers and consumers without wasting CPU time or losing wakeups.

1.1 State, a mutex, and a place to sleep

A mutex answers “Who may access this data now?” It does not answer “Has the work finished?” To wait for an event, a thread needs a record of the event and a way to sleep until that record changes.

Why spinning is a poor wait

The loop while (done == 0) ; spends CPU time repeatedly checking a flag. On one CPU, that work delays the child that must set the flag. In C, an unsynchronized read of an ordinary variable while another thread writes it is also a data race. The compiler need not reload the flag on each iteration.

A condition variable gives threads a waiting queue. POSIX provides pthread_cond_t, initialized with PTHREAD_COND_INITIALIZER or pthread_cond_init(). The queue is separate from the state being tested: done records completion; the condition variable provides somewhere to sleep.

Keep the check and the wait together

pthread_cond_wait(&c, &m) requires the caller to hold mutex m. It atomically releases that mutex and starts waiting. Before returning, it reacquires the mutex. This closes the dangerous gap in which another thread could change the state and signal before the waiter has actually started waiting.

Here is a parent waiting for a child's completion. The names lock, wait, signal, and unlock shorten the corresponding thread-library calls.

/* Parent */
lock(&m);
while (done == 0)
    wait(&c, &m);
unlock(&m);

/* Child */
lock(&m);
done = 1;
signal(&c);
unlock(&m);

Initially done is 0. If the parent runs first, it locks, finds unfinished work, and waits. Waiting releases m, allowing the child to lock it, set done, and signal. The parent eventually reacquires m, checks done again, and continues.

If the child runs first, it records completion before the parent arrives. Its signal wakes nobody, but the later parent sees done == 1 and skips the wait. Correctness must survive both orders.

Signals do not store events

pthread_cond_signal(&c) unblocks at least one waiter when there are waiters; POSIX permits more than one to wake. With no waiters it has no effect. pthread_cond_broadcast(&c) wakes all current waiters. Neither call deposits a saved notification for future threads.

Leaving out done therefore breaks the join pattern: a child can signal before its parent waits, after which the parent sleeps with no future signal coming. This is a lost wakeup. Checking done outside the mutex creates another lost-wakeup window: the parent checks false, the child sets true and signals, then the parent starts waiting.

Protect the state check and every state change with the same mutex. Hold it from the check through the call to wait. A straightforward signaling path changes the state and signals while still holding that mutex. POSIX permits signaling without holding it, but the shared-state protocol must still prevent a lost wakeup.

Key Takeaways

  • The state records the event; the condition variable supplies a waiting queue.
  • Call wait with the mutex held; wait releases it atomically and reacquires it before returning.
  • Protect the check and the state change with the same mutex.
  • A signal sent with no waiters is not saved.

1.2 Recheck after waking

Returning from wait means the thread may try again. It does not mean the condition it needs is still true. This distinction matters whenever several threads compete for the same data.

Mesa and Hoare semantics
SemanticsWhat a signal gives the waiter
MesaReadiness to run; the waiter must reacquire the mutex and recheck the state.
HoareAn immediate handoff of the lock and execution, preserving the signaled condition at that handoff.

POSIX, Java, C++ condition variables, and Python conditions follow Mesa behavior. Hoare's monitors introduced the stronger handoff model; Mesa's approach is the common practical choice.

Suppose C1 waits for a one-slot buffer to become nonempty. A producer fills it and signals C1. C1 is now ready, but C2 can acquire the mutex first and take the item. When C1 finally reacquires the mutex, the buffer is empty again.

lock(&m);
while (count == 0)
    wait(&filled, &m);
v = get();
unlock(&m);

The while repeats the test after every return. An if tests only before the first wait and would let C1 call get() on an empty buffer. Even with one producer and one consumer, use while: POSIX also allows spurious wakeups, where wait returns without a matching signal.

Wake one thread or all of them?

A signal is suitable when the waiters need the same condition and any one can use the change. A broadcast is useful when several can proceed, or when different waiters need different amounts and the signaler cannot select the right one.

Consider an allocator with 0 bytes free. Ta needs 100 bytes and Tb needs 10. Another thread releases 50 bytes. A signal might wake Ta, which still cannot proceed, leaving Tb asleep even though its request fits. Broadcasting wakes both. They reacquire the mutex one at a time; Tb can allocate, while Ta waits again. This is a covering condition: the notification covers every potentially eligible waiter.

Broadcast can cause extra wakeups, context switches, and competition for the mutex. Separate condition variables for distinct predicates can make signaling more selective. In a buffer, producers and consumers should not share one undifferentiated signal queue.

The monitor connection

A monitor bundles shared data with operations that admit one thread at a time and condition variables for waiting. Java objects supply a lock and a wait set. Inside synchronized, a thread can call wait(), notify(), or notifyAll(); calling them without owning that object's monitor throws IllegalMonitorStateException.

Waiting releases only the associated mutex or Java object's lock. If a thread also holds another lock, that second lock stays held. A producer that needs it may be unable to make the condition true. Check all locks held at a wait, not just the one named in the call.

Key Takeaways

  • Under Mesa semantics, waking makes a thread ready; it still competes for the mutex.
  • Wait in a while loop to handle changed state and spurious wakeups.
  • Broadcast when one signal could leave an eligible waiter asleep.
  • Separate waiting conditions can use separate condition variables.
  • Waiting releases only its associated lock.

1.3 Build a bounded buffer with two waiting queues

Producers add items to a fixed-size buffer; consumers remove them. A correct bounded buffer prevents overwriting a full buffer, removing from an empty one, and concurrent changes to its bookkeeping. A web server's request queue and the pipe between grep error log.txt and wc -l both fit this pattern.

Follow the ring indices

A ring buffer uses an array and two indices. fill_ptr marks the next write; use_ptr marks the next read. Both advance modulo MAX. A separate count distinguishes empty from full, even when the two indices are equal.

void put(int v) {
    buffer[fill_ptr] = v;
    fill_ptr = (fill_ptr + 1) % MAX;
    count++;
}
int get(void) {
    int v = buffer[use_ptr];
    use_ptr = (use_ptr + 1) % MAX;
    count--;
    return v;
}

Start with capacity 3 and both indices at 0. Putting 7, 8, and 9 fills slots 0, 1, and 2. The write index wraps to 0 and count reaches 3. Getting returns 7 and advances the read index to 1. Putting 10 reuses slot 0. The array is now [10, 8, 9], but FIFO removal follows the read index: 8 comes next, then 9, then 10.

Match each signal to the opposite role

Use one mutex, empty for producers waiting for space, and fill for consumers waiting for an item. Initialize both condition variables and the mutex before starting threads.

/* Producer: repeat for each item v */
lock(&m);
while (count == MAX)
    wait(&empty, &m);
put(v);
signal(&fill);
unlock(&m);

/* Consumer */
lock(&m);
while (count == 0)
    wait(&fill, &m);
v = get();
signal(&empty);
unlock(&m);

The check and buffer update stay in one critical section. Otherwise two producers could both see one free slot and overwrite each other's work. Waiting releases m, so the opposite role can change the buffer.

Why one queue can stall

With one condition variable and signals, producers and consumers can wake the wrong role. For capacity 1, C1 and C2 first sleep on empty. P inserts an item, wakes C1, then waits because the buffer is full. C1 removes the item and signals, but the signal may wake C2 instead of P. C1 waits again; C2 also finds empty and waits. P stays asleep although space is available. Two queues route each signal to the role that can now proceed. Broadcasting on a shared queue can avoid that selection problem, at the cost of extra wakeups.

A larger capacity absorbs bursts and allows producing and consuming work outside the buffer's short critical section to overlap. The buffer updates themselves remain serialized. Capacity 1 forces every insertion and removal to alternate.

For clean shutdown, first wait for every producer to finish its real items. Then enqueue one reserved end-of-data item per consumer, such as -1 when real items are nonnegative. Each consumer exits after taking one. Sending those markers while producers are still active can leave real items with no consumer.

Key Takeaways

  • Advance ring indices modulo capacity; count distinguishes empty from full.
  • Producers wait for space and signal items; consumers wait for items and signal space.
  • Use while loops and one mutex around the predicate and buffer update.
  • One shared signal queue can wake the wrong role and leave all threads asleep.
  • Finish all producers before enqueuing one end marker per consumer.
  • FIFO order follows the read index, not the physical array order.
2

Count permits with semaphores

A semaphore records permits that threads acquire and return. Its starting value lets the same primitive enforce exclusive access, remember an event, or limit a pool of resources. Follow its value through waits and posts, then combine permit counts with a mutex to build a bounded buffer. A readers–writers design adds another use: sharing access among readers while keeping writers exclusive, with a fairness cost to examine.

2.1 Choose a semaphore’s starting value

A semaphore holds an integer changed by atomic wait and post operations. Dijkstra introduced it in the 1960s as P and V; POSIX names the operations sem_wait() and sem_post(). Atomic means two threads cannot partially perform the same counter operation at once.

Available units determine the initial value

sem_init(&s, 0, X) creates an unnamed semaphore with value X. The second argument 0 selects sharing among threads in one process. For sharing between processes, a nonzero argument requires the semaphore to live in shared memory.

PurposeInitial valueMeaning
Lock1One thread may enter immediately.
Event ordering0The event has not occurred.
Resource poolNN units can be acquired now.

A binary semaphore used as a lock surrounds a critical section with wait and post. Starting it at 0 blocks the first entrant. Starting it at 2 allows two entrants and fails to provide mutual exclusion.

Two equivalent counting models

In Dijkstra's definition, the value stays nonnegative: wait sleeps until it can decrement without going below zero, and post adds a unit. Linux follows this nonnegative convention.

For hand traces, use the signed-counter model: wait decrements and blocks if the new value is negative; post increments and wakes a waiter when one is blocked. A negative value's magnitude counts blocked waiters in this model. It is a reasoning convention, not a negative value to expect from Linux's semaphore API.

Trace a lock initialized to 1:

  1. T0 waits: 1 becomes 0, so T0 enters.
  2. T1 waits: 0 becomes −1, so T1 blocks.
  3. T0 posts: −1 becomes 0, allowing T1 to continue.
  4. T1 finishes and posts: 0 becomes 1.

The wakeup transfers the existing permit to T1; another wait is not needed before T1 enters its critical section.

A post is remembered

With no waiter, a post increases the available count. A future wait can consume that permit. This is useful when one thread finishes before the thread waiting for it has started. A condition-variable signal has no such memory.

Semaphores have no owner: one thread may wait and another may post. A mutex belongs to its locking thread; unlocking a default POSIX mutex from another thread is undefined behavior. The ownerless semaphore supports signaling, but also makes an extra post harder to diagnose.

A semaphore can be built from a mutex, condition variable, and nonnegative integer. Wait locks, loops while value <= 0, waits, decrements, and unlocks. Post locks, increments, signals, and unlocks. The mutex makes the counter operations atomic; the loop handles wakeups that do not leave a unit available. Locks with condition variables and semaphores can implement one another, although a correct condition-variable implementation from semaphores is more involved.

Key Takeaways

  • Initialize a semaphore to the number of units available immediately.
  • A lock semaphore starts at 1 and pairs wait before the section with post after it.
  • The signed-counter trace model uses negative values for blocked waiters; Linux values stay nonnegative.
  • Posts are remembered as permits, even without current waiters.
  • A semaphore has no owner; a mutex does.

2.2 Remember events and limit resource use

The same wait/post pair can represent an event or a limited supply. Decide what a permit means before choosing the initial count and where each operation belongs.

Make completion precede use

For a parent waiting for a child, initialize finished to 0. The child posts after finishing its work. The parent waits before using the result.

/* Child */
produce_result();
sem_post(&finished);

/* Parent */
sem_wait(&finished);
use_result();

If the parent arrives first, the signed value changes from 0 to −1 and it sleeps. The child's post returns the value to 0 and wakes it. If the child arrives first, its post changes 0 to 1; the later wait changes 1 to 0 without blocking. One completed event permits one successful wait.

Count a resource pool

A pool of four database connections uses a semaphore initialized to 4. Each client waits before borrowing a connection and posts after returning it. A fifth simultaneous borrower sleeps. The semaphore counts admission; a separate mutex still protects the shared list that identifies which connection is free. Two admitted threads must not remove the same list entry.

Throttling applies the same idea to an expensive phase. Suppose 100 threads each need 500 MB during processing. Together they could need 50,000 MB. A machine with 8 GB has 8,000 MB under decimal SI units. Admitting at most 12 workers limits this phase to 6,000 MB and leaves 2,000 MB for other use.

sem_wait(&processing_slots);
run_memory_heavy_phase();
sem_post(&processing_slots);

Every path after a successful wait must return its permit, including an error path. Forgetting a post gradually reduces capacity. Posting twice creates an extra permit and allows more simultaneous users than intended.

Meet at a rendezvous

A rendezvous requires two threads to arrive before either continues. Initialize a_arrived and b_arrived to 0.

/* A */
sem_post(&a_arrived);
sem_wait(&b_arrived);

/* B */
sem_post(&b_arrived);
sem_wait(&a_arrived);

If A arrives first, it records its arrival and blocks for B. B records its arrival and can consume A's saved permit. A can then consume B's permit. Reversing both sequences to wait before post leaves each thread asleep before it announces anything.

These two semaphores implement one meeting. Reusing synchronization across repeated rounds requires keeping rounds from consuming each other's arrivals; one successful trace is not a reusable barrier design.

A semaphore fits a count or a simple event well. A condition variable fits a richer predicate, such as waiting until a queue contains several items of a required type. In that case the mutex protects the state from which the condition is computed.

Key Takeaways

  • Start an ordering semaphore at 0 and post only after the event it represents.
  • A counting semaphore limits admission but does not protect the resource list.
  • Throttling bounds simultaneous demand for memory or another limited resource.
  • For a two-thread rendezvous, announce arrival before waiting for the other thread.
  • An extra post creates capacity; a missing post removes capacity.

2.3 Reserve a slot before locking the buffer

A semaphore buffer separates two questions: “Is an item or slot available?” and “Who may update the array now?” Two counting semaphores answer the first question, and a mutex answers the second.

Set up three distinct controls

For capacity MAX, initialize empty to MAX, full to 0, and the mutex to unlocked. Here empty and full are semaphores, not condition variables. A permit from empty reserves one slot; a permit from full reserves one item.

/* Producer */
sem_wait(&empty);
lock(&mutex);
put(v);
unlock(&mutex);
sem_post(&full);

/* Consumer */
sem_wait(&full);
lock(&mutex);
v = get();
unlock(&mutex);
sem_post(&empty);

The producer acquires space before touching the ring, then publishes the completed item by posting full. The consumer acquires an item, removes it under the mutex, and returns space with a post to empty. A successful semaphore wait already reserves the resource, so these calls do not need a condition-variable-style predicate loop around them.

Trace one slot

Initially empty is 1 and full is 0. A consumer waits on full, changing it to −1 in the signed model, and sleeps. The producer waits on empty, taking it from 1 to 0. It locks, inserts, unlocks, and posts full, taking −1 to 0 and releasing the consumer.

The producer's next wait on empty changes 0 to −1 and blocks. The consumer takes the item under the mutex and posts empty, taking −1 to 0 and releasing the producer. The semaphore counts carry the handoff; no separate count is required for synchronization.

With a larger buffer, several producers can pass the space semaphore. Without the mutex, both might read the same fill_ptr and write the same slot. A reservation is not exclusive access to the array's indices.

Why locking first can deadlock

Suppose the consumer locks mutex before waiting on full. On an empty buffer it sleeps while still owning the mutex. The producer needs that mutex to insert the item whose post would release the consumer. Neither can finish.

A semaphore wait releases no mutex. A condition-variable wait deliberately releases its associated mutex. The order in this buffer therefore matters: reserve first, lock second, change the buffer, unlock, then publish the opposite permit.

The broader rule is to avoid blocking while holding a lock needed by the operation that can unblock you. That rule explains why the readers–writers algorithm can have a different safe arrangement: its writer does not need the reader-counter lock to release its resource.

The mutex may itself be a binary semaphore initialized to 1. Its role remains mutual exclusion; the other two semaphores count resources.

Key Takeaways

  • Initialize empty to capacity, full to 0, and the mutex to unlocked.
  • Reserve space or an item before acquiring the buffer mutex.
  • Protect ring indices and array updates even when permits are counted correctly.
  • Waiting on a semaphore releases no mutex.
  • Post full after inserting; post empty after removing.
  • A blocked waiter must not hold a lock its unblocking partner needs.

2.4 Let readers share and writers work alone

Read-heavy structures can allow several readers at once because reading does not change the data. A readers–writers lock admits either a group of readers or one writer. Every change to the protected data requires write access.

Build the reader group

Use readers = 0, a semaphore lock initialized to 1 to protect that counter, and writelock initialized to 1 to protect the shared resource.

/* Acquire read access */
sem_wait(&lock);
readers++;
if (readers == 1)
    sem_wait(&writelock);
sem_post(&lock);

/* Release read access */
sem_wait(&lock);
readers--;
if (readers == 0)
    sem_post(&writelock);
sem_post(&lock);

A writer waits on writelock, changes the data, then posts writelock. It does not acquire the counter lock. The first reader takes writelock for the group, and the last reader returns it. A semaphore suits that handoff because its posting thread need not be the thread that originally waited.

Trace R1 entering: it raises readers to 1 and takes writelock. R2 raises the count to 2 without taking another permit. Writer W now blocks on writelock. R1 leaves and lowers the count to 1. R2 leaves, lowers it to 0, and posts, allowing W to write alone.

The first reader may wait for writelock while holding lock. This does not create the bounded-buffer deadlock: the current writer needs no counter lock to finish and post writelock.

Fairness is a separate property

This design prefers readers. While the reader count stays positive, another reader can join the group ahead of a waiting writer. A steady stream of overlapping readers can keep the writer waiting indefinitely: writer starvation.

PolicyAdmission choiceRisk
Reader preferenceNew readers may join active readers.Writers can starve.
Writer preferenceWaiting writers stop new readers entering.Readers can starve.
Fair admissionArrivals receive access in a fair order.More admission bookkeeping.

The basic design provides safety but no bound on how long a writer waits. More reader throughput is not the same as fair service.

Use the correct mode

A read lock cannot protect a write from other readers. Updating even a cache counter under read access can race with another reader. To upgrade in this basic design, release read access, acquire write access, and recheck the condition. Waiting for write access while still counted as a reader can make the thread wait for itself forever.

POSIX provides pthread_rwlock_t, pthread_rwlock_rdlock(), pthread_rwlock_wrlock(), and pthread_rwlock_unlock(). Java offers ReentrantReadWriteLock; C++17 offers std::shared_mutex. These locks cost more bookkeeping than a plain mutex. Measure the workload: short sections or frequent writes can make a simple mutex as fast or faster.

Key Takeaways

  • A readers–writers lock permits many readers or one exclusive writer.
  • The first reader acquires the group permit; the last reader returns it.
  • The reader counter needs its own lock.
  • Reader preference can starve a waiting writer.
  • Release read access before requesting write access, then recheck the data.
3

Find the ordering assumption behind a bug

Many concurrency failures let the program keep running with the wrong data. Two recurring causes are treating several accesses as one indivisible action and assuming one thread's work precedes another's. Tracing a few carefully chosen accesses often exposes the hidden assumption. The fix depends on that assumption: protect a whole operation when it must stay together, and establish an explicit dependency when one operation must happen first.

3.1 Protect the entire check and action

An atomicity violation occurs when code relies on a group of accesses being indivisible, but another thread can act between them. Each individual access can look reasonable while the combined operation fails.

Find the gap after the check

Consider a shared log buffer:

/* T1 */
if (log->buf != NULL)
    write_out(log->buf);

/* T2 */
log->buf = NULL;

T1 can check a non-null pointer, pause, and resume after T2 clears it. T1 then passes null to write_out(). The intended guarantee was not just a valid check; it was that the checked value stayed usable through the call.

Protect the check and use together, and protect the competing assignment with the same mutex:

/* T1 */
lock(&m);
if (log->buf != NULL)
    write_out(log->buf);
unlock(&m);

/* T2 */
lock(&m);
log->buf = NULL;
unlock(&m);

T2 now runs either before or after T1's whole operation. Giving each thread its own lock does not help: both could hold their separate locks simultaneously. Locking only T1 leaves T2 free to enter the gap.

A valid test can still lead to an invalid balance

Start with balance 50. Two threads each intend to withdraw 30 only if enough remains. Both can read 50 and pass balance >= 30 before either acts. If the withdrawals are then applied, the balance can become −10. Protecting the test and subtraction together lets the first withdrawal leave 20; the next thread sees 20 and refuses.

Even atomic loads and atomic subtractions do not make that pair one transaction. Another thread can act between the atomic check and the atomic change. One lock around the entire decision and change enforces the required boundary.

The same issue appears in count++: loading, adding, and storing are separate stages. Two threads can load the same old value and store the same incremented value, losing an update. A lock around the increment or an atomic fetch-and-add performs the required update safely.

Use evidence without overgeneralizing

Lu and colleagues examined 105 fixed concurrency bugs in MySQL, Apache, Mozilla, and OpenOffice in a 2008 study. Of these, 74 were non-deadlock bugs and 31 were deadlocks. Atomicity and order violations accounted for 97% of the non-deadlock sample. These are findings about that studied sample, not a percentage guaranteed for every codebase.

About 92% of the studied bugs could be triggered by controlling the order of at most four memory accesses. A small interleaving table can therefore be more revealing than thousands of successful ordinary runs. ThreadSanitizer helps find executed data races, but a logical atomicity bug can remain even when every individual access is synchronized. Test the whole operation's invariant, such as “a successful withdrawal never overdraws the balance.”

Key Takeaways

  • Choose the critical section around the whole operation that must be indivisible.
  • Every competing access must follow the same locking protocol.
  • Atomic individual accesses can still leave a check-then-act gap.
  • Trace a small harmful interleaving instead of relying only on repeated successful runs.

3.2 Make one operation happen before another

An order violation means operation A must happen before operation B, but the program provides no synchronization that guarantees it. Mutual exclusion alone cannot choose which operation goes first.

A thread can run immediately

Suppose a creator stores a newly created worker record in a shared pointer, while the worker starts by using that pointer:

/* Creator */
worker = thread_create(worker_main);

/* New worker */
use(worker->state);

The worker may run before thread_create() returns and before the assignment completes. The creator's source-code order says nothing about when the new thread gets CPU time. A successful run simply means the creator happened to win that race.

Publish readiness after initialization

Add ready = 0, mutex m, and condition variable c. The creator keeps the mutex while it assigns the pointer and publishes readiness:

/* Creator */
lock(&m);
worker = thread_create(worker_main);
ready = 1;
signal(&c);
unlock(&m);

/* Worker, before using worker */
lock(&m);
while (ready == 0)
    wait(&c, &m);
unlock(&m);
use(worker->state);

If the worker reaches the mutex first, it cannot pass the readiness test until the creator has published the record. If it arrives later, ready remembers that initialization finished. Keep the record alive while the worker uses it, and synchronize any later changes.

A semaphore initialized to 0 offers a shorter handoff: the creator posts after assigning the pointer, and the worker waits before using it. For a result that exists only after a thread finishes, joining that thread before reading its result establishes the needed order. Freeing an object likewise must wait until its users finish, for example through joins or a correctly synchronized reference count.

Why timing changes do not fix it

Adding sleep(1) or sched_yield() only changes the chance of the desired schedule. A slower or loaded system can still let the consumer proceed too early. A mutex around each operation prevents overlap but still allows the use to acquire the mutex before initialization.

Hidden assumptionBug typeNeeded guarantee
“Nothing changes between this check and action.”Atomicity violationOne protected operation.
“Initialization finishes before this use.”Order violationAn explicit completion or readiness dependency.

In Lu and colleagues' study, about one third of the non-deadlock bugs were order violations, and about 73% of the non-deadlock bugs were not fixed by merely adding or changing locks. Before choosing a primitive, name the operation that must finish and the operation that must wait. That dependency determines where the post, signal, or join belongs.

Key Takeaways

  • An order violation assumes an earlier action has finished without enforcing that order.
  • Publish readiness only after initialization, and wait before using the result.
  • A lock prevents overlap but does not determine who goes first.
  • Sleep and yield change timing without guaranteeing a dependency.
  • Keep shared objects alive until all users finish.
4

Reason about deadlock and progress

Deadlock turns a set of ordinary waits into a permanent standstill: each thread needs something another blocked thread holds. A dependency graph makes that cycle visible. The four necessary conditions suggest ways to prevent it; advance resource claims support avoidance; and a detector can find a deadlock after it forms. Compare these strategies by the knowledge they need, the concurrency they cost, and the work recovery may lose.

4.1 Follow the cycle of waiting threads

A set of threads is deadlocked when each waits for something only another thread in the set can provide. No member can continue and perform the action that releases the others. The program may work in many schedules before one unlucky ordering exposes the cycle.

Build the two-lock failure

T1 acquires L1 and next requests L2. T2 acquires L2 and next requests L1. If T1 takes L1, then T2 takes L2, both second acquisitions block. Each thread holds exactly the resource its partner needs.

This can hide inside ordinary functions. If transfer(a, b) locks a and then b, simultaneous calls transfer(x, y) and transfer(y, x) can acquire the account locks in opposite orders. Encapsulation makes a call convenient but can conceal the locks it takes.

Identify all four conditions
Necessary conditionMeaning in the lock example
Mutual exclusionOnly one thread holds a given lock.
Hold-and-waitEach holds one lock while waiting for another.
No preemptionA held lock is not forcibly taken away.
Circular waitEach waits for the next thread's held lock.

All four conditions are necessary. Designing out any one of them prevents resource deadlock. The presence of exclusive locks and nested acquisition alone does not establish that a particular execution has reached a cycle.

Read a wait-for graph

Draw a node per thread. Draw T → U when T waits for a resource U holds. With single-instance resources such as exclusive locks, a cycle identifies deadlock.

For T1 holding A and waiting for B, T2 holding B and waiting for C, and T3 holding C and waiting for A, the graph is T1 → T2 → T3 → T1. All three are stuck. If T3 instead requests a free lock D, it can finish and release C, allowing T2 and then T1 to proceed. A chain is not a cycle.

When resource types have multiple interchangeable instances, a cycle alone is insufficient: an additional instance may become available. Use resource counts and outstanding requests to assess that case.

Dining philosophers and related failures

Five philosophers share five forks arranged in a circle. Each needs both neighboring forks. If all pick up their left fork first, all can wait for their right fork forever. Reversing one philosopher's acquisition order breaks the cycle. Limiting admission to four philosophers also prevents the all-five holding pattern; at least one admitted philosopher can obtain both forks.

Starvation means one thread waits indefinitely while others progress. Livelock means threads run and react but repeatedly fail to make progress. A deadlocked set is blocked. One thread can even deadlock itself by locking a normal, non-recursive mutex it already holds. Semaphores and condition variables can create dependencies too: a waiter holding a lock needed by its signaler can close a cycle without a second explicit lock acquisition.

Key Takeaways

  • Deadlock is a permanent dependency cycle among blocked threads.
  • All four conditions must hold: exclusion, hold-and-wait, no preemption, and circular wait.
  • For single-instance resources, a wait-for cycle identifies deadlock.
  • A multiple-instance resource cycle requires further analysis.
  • Starvation, livelock, and deadlock describe different failures of progress.
  • Hidden locking inside functions can reverse acquisition order.

4.2 Remove a condition that permits deadlock

Prevention changes the design so a necessary deadlock condition cannot arise. Lock ordering is often the clearest approach because it attacks the circular dependency directly.

Give every lock an order

Assign unique keys to locks and always acquire lower keys before higher ones. For account transfers, sort the account identifiers before locking. Both transfer(x, y) and transfer(y, x) then acquire the same account first. If the identifiers name the same account, acquire its lock once.

A cycle would require the keys to keep increasing around the circle and return to the starting key, which is impossible. Every acquisition path must follow the rule. One helper that reverses the order restores the risk. Linux's lockdep tracks observed locking dependencies and reports potentially unsafe orders.

Acquire all or acquire none

A strict way to remove hold-and-wait is to grant a thread its complete resource set in one step, or grant nothing. It never holds a partial set while waiting for the remainder. This requires knowing the needed resources in advance and can reduce concurrency.

A related protocol serializes acquisition sequences with a global prevention lock:

lock(&prevention);
lock(&L1);
lock(&L2);
unlock(&prevention);
/* use L1 and L2, then release them */

All threads must finish acquiring their full set under that global lock, and resource release must not require acquiring it. One thread may still hold L1 while waiting for L2, so this does not literally remove hold-and-wait. It prevents other threads from starting a competing partial acquisition. No holder can be midway through a second competing acquisition sequence. Holding locks earlier and serializing acquisition are the costs.

Give up and retry

pthread_mutex_trylock() returns immediately with EBUSY when a lock is unavailable. A thread can acquire L1, try L2, and release L1 if the attempt fails. It must undo other partial acquisitions before retrying too.

Two threads can repeatedly collide, back off, and retry together. That is livelock: both execute, but neither finishes. Random backoff reduces repeated collisions; it does not turn the retry loop into a fairness guarantee.

Replace a lock with an atomic operation

Compare-and-swap (CAS) changes a location only if it still matches an expected value. Conceptually, an atomic-add retry loop reads the old value, attempts to replace it with old plus the increment, and retries if another thread changed it first. Actual C implementations use atomic types and atomic operations for the read as well as the update.

Removing lock acquisition removes lock deadlock from that operation. A lock-free algorithm guarantees system-wide progress, but one thread may keep losing its retries. General lock-free structures also need careful memory-lifetime design.

TechniqueMain trade-off
Ordered acquisitionEvery code path must obey the order.
All-at-once acquisitionResources must be known and may sit unused.
Trylock and retryRetry work and livelock need attention.
Lock-free updateMore demanding correctness reasoning.

Key Takeaways

  • A common lock order prevents circular wait only when all paths follow it.
  • True all-or-none acquisition removes hold-and-wait.
  • A global acquisition lock serializes complete acquisition sequences.
  • On a failed trylock, release partial holdings before retrying.
  • Random backoff reduces livelock collisions.
  • Lock-free progress for the system does not guarantee progress for each thread.

4.3 Test a resource request for safety

Avoidance checks whether a proposed request leaves a way for every thread to finish. It permits the usual resource-use patterns, but delays requests that destroy the remaining safe completion orders.

Safe is a promise of a possible order

A safe sequence orders threads so each can obtain its remaining maximum demand from the resources currently free plus those earlier threads release. An unsafe state lacks such a guaranteed order. It need not already be deadlocked: threads might request less than their maximum or release resources early.

Scheduling can also avoid dangerous overlap by ensuring two conflicting tasks do not enter their resource-use phases together. Simply placing both on one CPU is insufficient because they can still be preempted and interleave while holding resources. The required guarantee is nonoverlap of the conflicting phases.

Set up the banker's tables

For each resource type, track Available. For each thread, track Max, Allocation, and Need = Max - Allocation. Compare vectors component by component, never by their summed totals.

Use resource totals (7, 5, 6):

ThreadAllocationMaxNeed
T0(1, 1, 0)(5, 3, 2)(4, 2, 2)
T1(2, 0, 1)(3, 2, 2)(1, 2, 1)
T2(1, 1, 2)(2, 2, 4)(1, 1, 2)
T3(0, 1, 1)(4, 2, 3)(4, 1, 2)

Allocations total (4, 3, 4), leaving Available = (3, 2, 2).

Work through the safety test

Set Work to Available. Choose an unfinished thread whose Need fits Work, pretend it finishes, and add its Allocation back to Work.

  1. T1 fits (3, 2, 2). Returning (2, 0, 1) gives Work (5, 2, 3).
  2. T0 fits. Returning (1, 1, 0) gives (6, 3, 3).
  3. T2 fits. Returning (1, 1, 2) gives (7, 4, 5).
  4. T3 fits. Returning (0, 1, 1) gives (7, 5, 6).

Every thread finishes in this simulation, so the state is safe. Work increases by Allocation, not Max: the extra resources temporarily granted for completion are returned as well, giving a net gain equal to what the thread originally held.

Pretend to grant, then test again

A request must fit both the thread's Need and Available. Then subtract it from Available, add it to Allocation, subtract it from Need, and run the safety test on that tentative state.

From the original table, T2 requesting (1, 0, 1) leaves Available (2, 2, 1). T1 can still go first, followed by T0, T2, and T3, so it is safe.

Instead, T3 requesting (2, 1, 1) leaves Available (1, 1, 1). T3's remaining Need is (2, 0, 1). No thread fits: T0 needs 4 of A, T1 needs 2 of B, T2 needs 2 of C, and T3 needs 2 of A. Delay this request despite the currently free resources.

Every later request needs the same test. Avoidance needs declared maximum demands and a known resource model, making it more suitable for predictable specialized systems than general application workloads.

Key Takeaways

  • Need equals maximum claim minus current allocation.
  • A safe sequence must satisfy each resource type separately.
  • After simulating completion, add Allocation to Work.
  • A request must fit Need, fit Available, and leave a safe state.
  • Unsafe means no guaranteed completion sequence; it does not mean deadlock has already formed.

4.4 Detect a deadlock and choose recovery

A system can allow deadlock to form, detect it, and recover. This accepts occasional lost work in exchange for fewer restrictions on ordinary resource requests. Detection is useful only when there is a viable recovery action.

Find blocked cycles

For single-instance resources, maintain a wait-for graph and search for a cycle, for example with depth-first search. Suppose the edges are T1 → T2, T2 → T3, T3 → T1, and T4 → T2. T1, T2, and T3 are the cycle. T4 is blocked behind it; T4's own execution cannot release a resource that breaks that cycle.

With multiple instances, a cycle is insufficient. Use Available, Allocation, and each thread's current outstanding Request. Start Work at Available. Repeatedly find a request that fits, simulate completion, and add that thread's Allocation to Work. Unlike avoidance, this test uses what threads are waiting for now, not their possible future maximum demand.

The standard resource detector initially sets aside threads holding no resources: they cannot contribute held resources to a resource cycle. Among resource holders, those left unfinished by the test are deadlocked under the model. Other threads can still be unable to progress because they depend on that set. Keeping the cycle and its blocked dependents separate helps choose an effective recovery victim.

Recover without corrupting shared state

A system may terminate one or more processes, reclaim resources with rollback to a checkpoint, or restart. Choose victims by recovery cost, priority, work already done, and resources held. After removing one victim, check again: several independent cycles may require more than one recovery action.

Terminating an arbitrary thread is not enough if it leaves a mutex locked or a shared structure half changed. Recovery must release or reconstruct resources and restore valid state. Transaction systems can abort a deadlocked transaction because rollback is part of their design. An ordinary application often needs a controlled process restart instead.

Interpret timeouts carefully

pthread_mutex_timedlock() can return ETIMEDOUT after a deadline. A watchdog can reset a device that stops reporting progress. Both detect excessive waiting; a long wait can also come from slow I/O or scheduling. A timeout is evidence to investigate or recover from, not a unique diagnosis of a cycle.

StrategyWhat it needsMain cost
PreventionA design rule that removes a necessary condition.Less flexibility or concurrency.
AvoidanceFuture maximum demands and a safety check.Delayed requests and bookkeeping.
Detection and recoveryA detector and a valid recovery path.Checking cost and possible lost work.
Ignore rare deadlocksA tolerable failure and restart cost.Occasional disruption.

Linux and Windows do not generally find and repair cycles among an application's own locks. The program must implement its chosen strategy. If recovery repeatedly selects the same work as a victim, that work may starve even though each individual deadlock is broken.

Key Takeaways

  • Detection finds an existing deadlock; prevention stops it forming.
  • Multiple-instance detection uses current requests, not maximum Need.
  • Distinguish a resource cycle from threads blocked behind it.
  • Recovery must restore valid resources and shared state, not just stop a thread.
  • A timeout can also indicate a slow operation.
5

Keep an event loop responsive

Concurrency can come from one thread serving many tasks while their I/O waits overlap. An event loop chooses a ready task and runs a short handler, then returns to waiting for the next event. This removes locking between synchronous handlers on that loop, but makes every blocking operation everyone's delay. Readiness checks, asynchronous I/O, and saved continuations keep the loop moving; multiple cores and shared state still require care.

5.1 Handle ready descriptors without blocking

An event loop waits for events and dispatches short handlers on one thread. The program explicitly chooses which ready event to handle next. Node.js and nginx use event-based designs to serve many connections without assigning a dedicated thread to each connection.

while (1) {
    events = get_events();
    /* handle each ready event, then return to the loop */
}

Synchronous handlers on the same loop run one at a time. One handler's ordinary instructions are not interrupted by another handler on that loop. The loop still depends on handlers returning promptly.

Build and inspect select sets

select() accepts sets of descriptors to watch for reading, writing, and exceptional conditions, plus a timeout. Its nfds argument is one greater than the highest watched descriptor. Watching descriptors 3, 5, and 9 therefore needs nfds = 10, not 3.

FD_ZERO(&readset);
FD_SET(3, &readset);
FD_SET(5, &readset);
FD_SET(9, &readset);
int n = select(10, &readset, NULL, NULL, NULL);
/* If n > 0, test each watched descriptor with FD_ISSET. */

A null timeout waits for readiness; a zero timeout checks and returns immediately. select() overwrites the sets with the ready descriptors, so rebuild them before each call. Its positive return counts set bits across the returned sets; a descriptor ready in two sets counts twice. Zero means timeout; −1 means error.

Readiness is not data transfer. The program still calls read() or recv(). End of input can appear as readability; a read returning 0 means the peer has closed its sending side. Handle that result instead of repeatedly redispatching the same ended input.

Keep handlers short and non-blocking

Set sockets to O_NONBLOCK. When an operation cannot proceed, it returns −1 with errno set to EAGAIN or EWOULDBLOCK, allowing the handler to return to the loop. Readiness can change before the operation, so non-blocking mode also protects against an unexpected wait after a readiness report.

A blocking read inside a handler delays every queued event. With 100 queued requests and a 10 ms blocking disk read per handler, the last request waits about 990 ms before its own read begins; the batch takes about 1,000 ms. The thread spent most of that time waiting while ready work sat queued.

Choose a readiness interface
InterfaceHow the watched set is representedImportant property
select()Descriptor bit sets rebuilt for each call.Descriptor values must fit FD_SETSIZE, commonly 1024.
poll()An array of descriptors and requested/returned events.No corresponding fixed descriptor-value limit.
epoll / kqueueA registered interest set maintained by the kernel.Efficient event delivery for large watched sets.

select() and poll() examine the supplied watched set on each call, so their work grows with its size. Linux offers epoll; FreeBSD and macOS offer kqueue. Watching only a few descriptors does not make a high descriptor value safe for a fixed-size fd_set.

Key Takeaways

  • One loop dispatches synchronous handlers one at a time.
  • For select, nfds is the highest watched descriptor plus one.
  • Rebuild select sets after every call.
  • Readiness requires a later I/O call; read returning 0 means end of input.
  • Non-blocking sockets let handlers return when an operation would wait.
  • FD_SETSIZE restricts descriptor values, not just how many are watched.

5.2 Save the work that continues after I/O

Network readiness tells a loop when it can try I/O. Regular disk files are different: select() and poll() report them ready even when accessing their data still waits for storage. To keep serving other events, submit work asynchronously and handle its result later.

Submit, wait elsewhere, collect

POSIX asynchronous I/O stores a request in struct aiocb. Important fields are aio_fildes for the file descriptor, aio_offset for the file position, aio_buf for the destination, and aio_nbytes for the requested length.

/* cb describes a read request */
aio_read(&cb);
/* Return to other work. Later: */
int status = aio_error(&cb);
/* EINPROGRESS: still running; 0: succeeded; otherwise an error. */
/* After completion, aio_return(&cb) collects its result. */

aio_read() queues the request and returns. Completion is a separate event. When aio_error() indicates completion, aio_return() provides the result, such as bytes read after success. A request can fail, and a successful read can return fewer bytes than requested. The request record and its buffer must remain valid while the operation is outstanding.

Repeatedly scanning every request in a tight loop wastes CPU time. aio_sigevent can configure a completion signal or callback. Linux io_uring uses submission and completion queues; Windows I/O completion ports provide completion-based dispatch. These interfaces let the program act on completed work without constantly retrying every unfinished request.

Carry state across the gap

A blocked thread keeps its function's local variables on its stack. An event handler that submits I/O and returns must save what the later handler needs. This explicit bookkeeping is manual stack management; the saved state is a continuation.

Suppose a server reads file descriptor 12 for a client on socket 7. At submission it saves a record associating that read request with socket 7 and its response state. When completion arrives, the loop finds the record, checks the read result, and continues sending the response to the correct client. Separate records keep several outstanding requests from mixing up their destinations.

The continuation should be tied to the particular operation, not just an identifier that might later be reused. Keep the client reference valid until the completion is handled, and remove the record when the task finishes. Those lifetime details replace assumptions a straight-line blocking function could keep on its stack.

Readable syntax can hide a suspension

Callbacks, promises, and async/await automate much of this state saving. In JavaScript, const data = await readFile(path); suspends that function while its I/O is pending. Other loop work can run, and the function resumes later with its local state restored.

Asynchronous means the caller can continue before the operation completes. It does not mean that its handlers run on several CPU cores at once. It also does not make CPU-heavy code after an await yield automatically: that code still occupies its executing thread until it returns or suspends again.

Key Takeaways

  • Regular-file readiness does not guarantee a disk read will avoid waiting.
  • Asynchronous I/O separates submission from completion and result collection.
  • A continuation saves the state needed after the original handler returns.
  • Keep request records and buffers valid until completion.
  • Async/await saves state across suspension; it does not create multicore execution.

5.3 Choose events, threads, or a hybrid

An event loop is a good fit for many mostly idle connections with small bursts of work. A thread-based design keeps a task's control flow and stack together. The best choice depends on waiting time, CPU work, memory, and how the code will evolve.

Compare the work each design carries
ConcernEvent-based designThread-based design
Mostly idle connectionsSmall state record per connection.A thread has stack and scheduling overhead.
Blocking workOffload it or split around completion.The calling thread can wait while others run.
Several CPU coresRun several loops or use workers.Schedule runnable threads across cores.
Shared mutable dataOne synchronous loop avoids inter-handler overlap.Shared accesses need synchronization.
A function becomes blockingIts callers may need continuation restructuring.Straight-line control flow can remain.

A chat server with 50,000 mostly idle connections can benefit from event records instead of a dedicated thread stack per connection. An image processor doing heavy computation per request needs enough workers to use the available cores; one busy loop cannot make those computations parallel.

Hidden waits still matter

A handler can stall without an obvious blocking API call. Accessing a page that has been paged out may trigger a disk read. During that page fault, the loop thread waits. mlock() can keep needed pages resident when the system permits the requested locked-memory amount. Simply removing explicit blocking calls does not eliminate every source of delay.

Code changes can introduce another hidden wait. If a formerly immediate helper starts fetching data, an event-based caller must split its work around a continuation or offload the call. A blocking thread's stack already stores where to resume and the local state it needs.

Combine the strengths

A hybrid design keeps network readiness on the event loop and sends blocking or CPU-heavy tasks to a worker pool. Completions return to the loop for response handling. Node.js uses libuv's thread pool for file system operations, so an asynchronous interface can be implemented by background threads.

To use multiple cores, a server can run one event loop per core in separate threads or processes. If those loops share mutable data, they need the usual locks or another synchronization protocol. The no-lock benefit applies to synchronous handlers on one loop, not every part of an event-based server.

A task can interleave across await

Suppose one unit of stock remains. A handler checks availability, awaits payment, then subtracts stock. While it is suspended, another handler can check the same stock and also begin payment. Both checks may succeed before either subtraction. No two synchronous handlers run simultaneously, yet the overall check-and-update operation is not atomic.

Reserve stock as one synchronized state change before yielding, with a release path if payment fails, or recheck under a protocol that prevents a second sale. A mutex held across a long wait can create other progress problems, so design the state transition and its recovery explicitly. Event code still needs reasoning about which facts remain true across each suspension.

Key Takeaways

  • Events suit many idle connections; CPU-heavy work needs parallel workers.
  • One event loop uses one core; shared data across loops needs synchronization.
  • Page faults and newly blocking helpers can stall a loop.
  • Hybrid servers offload blocking work and return completions to the loop.
  • A task spanning await can suffer a check-then-act atomicity violation.

Condition Variables and Semaphores examples

5 Walkthroughs

1

From a completion flag to a working queue

12 steps
Trace sleeping, waking, rechecking, and routing notifications between producers and consumers.
  1. 1
    IntroductionWait without wasting a CPU

    A parent needs its child’s result, but the child may finish before or after the parent starts waiting. A working protocol must handle both orders.

    Use a shared done flag to remember completion, a mutex to protect it, and a condition variable to let the parent sleep. Follow where the mutex moves as each thread runs.

  2. 2
    ExampleThe parent arrives first

    Initially done = 0. The parent locks m, checks the flag, and calls wait(&c, &m) inside a while loop.

    Wait atomically releases the mutex and starts waiting. The child can now acquire m. There is no unlocked gap between checking the condition and joining the wait in which the child could send a notification that the parent misses.

  3. 3
    ExampleThe child records completion

    The child acquires m, stores done = 1, signals c, and unlocks. The signal makes the parent ready; it does not give the parent immediate execution.

    The parent reacquires m before wait returns, checks done again, and continues because the child’s completed state is now recorded. Both threads use the same mutex for the shared flag.

  4. 4
    ChallengeReverse their arrival order

    The child finishes and signals before the parent has reached its wait. What prevents the parent from sleeping forever?

    ---reveal---

    The child has already set done = 1 under the mutex. The parent checks that flag while holding the mutex and skips the wait. The early signal is not saved; the state flag carries the information across the different arrival order.

  5. 5
    ExampleA wakeup does not reserve an item

    C1 is waiting for a one-slot buffer. A producer inserts an item and signals C1. Before C1 reacquires the mutex, C2 acquires it and removes that item.

    Under Mesa semantics, C1 is only ready to compete. It must test the buffer again after acquiring the mutex. Returning from wait cannot stand in for checking whether the item still exists.

  6. 6
    ChallengeChoose the right recheck

    In that trace, C1 used if (count == 0) wait(...). It now returns after C2 emptied the slot. What code change prevents an invalid removal?

    ---reveal---

    Use while (count == 0) wait(...). After reacquiring the mutex, C1 sees the empty buffer and waits again. The same loop also handles spurious wakeups, so it is needed even without a second consumer.

  7. 7
    ExampleWake every eligible request

    An allocator has 0 bytes free. Ta waits for 100 bytes and Tb for 10. Releasing 50 makes only Tb eligible, but a signal might wake Ta.

    Broadcast reaches both. Each reacquires the mutex and checks its own requested amount. Tb can take 10; Ta waits again. The predicate loop provides correctness while broad notification avoids leaving an eligible thread asleep.

  8. 8
    ExampleKeep queue order through wraparound

    Capacity is 3. Insert 7, 8, 9; remove 7; insert 10. The physical array is now [10, 8, 9].

    The read index points at slot 1, so the removal order is 8, 9, 10. Advance both indices modulo capacity and change count under the mutex. The array’s left-to-right display is not necessarily the queue’s logical order.

  9. 9
    Key InsightGive each role its own queue

    A producer waits on empty while full, inserts, then signals fill. A consumer waits on fill while empty, removes, then signals empty. Both condition variables use the buffer mutex.

    The separate queues matter: an item notification reaches a consumer, and a space notification reaches a producer. One shared signal queue could choose a thread whose condition is still false.

  10. 10
    ChallengeFind the sleeping producer

    A one-slot buffer uses one shared condition queue and while loops. P sleeps on full. C1 removes the item, signals C2, then waits on empty. C2 also finds empty and waits. Why is space still unused?

    ---reveal---

    P never received the space notification. All runnable work has stopped although a producer could proceed. Separate producer and consumer queues route C1’s signal to P.

  11. 11
    ChallengeEnd work without losing items

    Two producers are still generating requests. A coordinator plans to add one stop marker for each consumer now. What must it change, and why?

    ---reveal---

    First wait for all producers to finish. Only then enqueue the stop markers. Otherwise consumers can take the markers and exit while producers still have real requests to insert. Normal queue synchronization is still used to insert and remove markers.

  12. 12
    RecapAudit a condition wait

    For each wait, identify the state, mutex, and queue. Keep the predicate check under the mutex, enter wait without an unlocked gap, and recheck after every return.

    Then inspect the signal path: did it change the state, and can the notification reach a thread that can now proceed? Those questions expose both lost wakeups and wrong-role buffer stalls.

2

Follow permits through real protocols

15 steps
Choose counts, trace handoffs, and keep resource reservations separate from mutual exclusion.

Save this lesson to follow every Walkthrough step by step.

  1. 1
    IntroductionName what one permit means
  2. 2
    ExampleTrace a binary semaphore
  3. 3
    ChallengeRepair a wrong initial count
  4. 4
    ExampleRemember an early finish
  5. 5
    ExampleLimit a memory-heavy phase
  6. 6
    ChallengeSeparate admission from bookkeeping
  7. 7
    ConnectionMeet before continuing
  8. 8
    ExplanationReserve, update, publish
  9. 9
    ExampleTrace a blocked consumer
  10. 10
    ChallengeSpot the lock dependency
  11. 11
    ExplanationLet a reader group hold one permit
  12. 12
    ExampleWatch the waiting writer
  13. 13
    ChallengeHandle a needed update
  14. 14
    ChallengeKeep both kinds of protection
  15. 15
    RecapAudit every acquired permit
3

Diagnose the hidden interleaving

9 steps
Distinguish an operation that must stay together from an operation that must happen first.

Save this lesson to follow every Walkthrough step by step.

  1. 1
    IntroductionAsk what the code assumes
  2. 2
    ExamplePlace a change inside the gap
  3. 3
    ExampleMake the numeric decision indivisible
  4. 4
    ChallengeFind the missing critical-section boundary
  5. 5
    ExampleA new worker can run first
  6. 6
    ExplanationPublish a completed state
  7. 7
    ChallengeReplace a timing guess
  8. 8
    ChallengeChoose between the two repairs
  9. 9
    RecapTest the guarantee, not the delay
4

From a lock cycle to a safe request

17 steps
Trace deadlock, remove a condition, test resource safety, and plan recovery.

Save this lesson to follow every Walkthrough step by step.

  1. 1
    IntroductionSeparate three strategies
  2. 2
    ExampleClose a two-thread cycle
  3. 3
    ExplanationName the necessary conditions
  4. 4
    ChallengeDistinguish a chain from a cycle
  5. 5
    ExampleOrder both directions the same way
  6. 6
    TipRelease partial progress before retrying
  7. 7
    ChallengeAudit the unseen helper
  8. 8
    ExampleCompute the remaining claims
  9. 9
    ExampleFind a thread that can finish first
  10. 10
    ExampleFinish the safety sequence
  11. 11
    Key InsightA free grant can still be unsafe
  12. 12
    ChallengeCompare resources by type
  13. 13
    ConnectionDetect the wait that exists now
  14. 14
    ExplanationRecover through a valid state
  15. 15
    ChallengeSelect a useful recovery target
  16. 16
    ChallengeInterpret an unsafe result
  17. 17
    RecapMatch the check to the question
5

Serve events while I/O is pending

13 steps
Build a readiness loop, save continuations, and locate the remaining concurrency risks.

Save this lesson to follow every Walkthrough step by step.

  1. 1
    IntroductionMove waiting to the loop boundary
  2. 2
    ExampleBuild select’s watched set
  3. 3
    ExplanationPerform the actual read
  4. 4
    ChallengeCheck a high descriptor
  5. 5
    ExplanationDo not confuse file readiness with completion
  6. 6
    ExampleSave the response destination
  7. 7
    ChallengeInterpret a pending operation
  8. 8
    ConnectionLet await preserve local state
  9. 9
    ExampleUse workers for expensive work
  10. 10
    TipWatch for an implicit wait
  11. 11
    ChallengeFind the gap across await
  12. 12
    ChallengeKeep the multicore boundary visible
  13. 13
    RecapAudit every suspension point

Condition Variables and Semaphores practice questions

50 Practice Questions in 5 sets

Set 1: Waiting, permits, and dependencies10 Practice Questions

Question 1 of 10

A service has seven identical idle device handles. What initial semaphore value allows all available handles to be borrowed?
  1. 0
  2. 1
  3. 7
  4. 8
Show answerHide answer
Answer: C
Initialize the semaphore to 7 because seven units are available immediately. Each borrower consumes one permit and returns it after use. Starting at 1 would serialize all borrowers even though the pool can serve seven; starting at 0 would block the first borrower.

Question 2 of 10

What loop should surround a POSIX condition wait when a consumer needs a nonempty queue?
  1. while (count == 0)
  2. if (count == 0)
  3. while (count > 0)
  4. if (count > 0)
Show answerHide answer
Answer: A
Wait while the queue is empty and recheck after every return. The queue may have changed before the awakened thread reacquires the mutex, and spurious wakeups are allowed. An if performs no second check, so it can let the consumer remove from an empty queue.

Question 3 of 10

What initial semaphore count allows exactly one thread into a critical section before a post?
  1. 0
  2. 2
  3. −1
  4. 1
Show answerHide answer
Answer: D
One initial permit lets the first entrant pass and makes a second wait for a returned permit. A count of 0 instead blocks the first thread, so it cannot reach the critical section and its later post. A negative initial value is not a valid POSIX semaphore initialization.

Question 4 of 10

A machine has 12 GB of RAM. Other work needs 3 GB, and each admitted worker needs 750 MB. Using decimal SI units, how many workers can fit in the remaining memory?
  1. 16
  2. 12
  3. 9
  4. 15
Show answerHide answer
Answer: B
The workers have 9 GB, or 9,000 MB, available after reserving 3 GB. Dividing 9,000 MB by 750 MB gives 12 simultaneous workers. Admitting 16 would spend the full 12 GB on workers and leave no memory for the other work specified.

Question 5 of 10

Two threads reserve four seats each. There are six seats left, and both availability checks finish before either reservation. What critical-section boundary prevents overbooking?
  1. A lock region for checking, then another for reserving.
  2. One lock region around checking and reserving.
  3. A read lock around each thread’s entire reservation.
  4. A separate private mutex around each reservation.
Show answerHide answer
Answer: B
The seat check must stay valid until its dependent reservation, so protect both inside one critical section shared by all reservers. Separate locked accesses still let both checks observe six available seats before either reserves four. A private mutex cannot exclude the competing thread.

Question 6 of 10

A reporting thread needs a final total written by a worker just before that worker exits. What operation establishes completion before the report reads the total?
  1. Sleep briefly before reading.
  2. Yield the CPU before reading.
  3. Lock only around the read.
  4. Join the worker before reading.
Show answerHide answer
Answer: D
Joining the worker waits until it has terminated, so the worker’s completed total is available before the report reads it. A sleep merely guesses how long the work takes. Locking the read alone cannot require the worker to have finished or written the result first.

Question 7 of 10

Thread H holds M and waits for N. Thread J holds N and waits for M. Both locks are exclusive and cannot be forcibly reclaimed. What describes these two threads?
  1. A deadlocked set.
  2. A starved group.
  3. A livelocked pair.
  4. A runnable pair.
Show answerHide answer
Answer: A
Each thread waits for a lock only its blocked partner can release, forming a permanent cycle. This is deadlock, with both threads blocked. Livelock would require them to keep running and reacting, while starvation would leave other work making progress past a postponed waiter.

Question 8 of 10

A transfer locks account objects with unique keys. What acquisition rule prevents opposite-direction transfers from creating circular wait?
  1. Acquire the source account before the destination.
  2. Acquire the destination account before the source.
  3. Acquire both accounts in increasing key order.
  4. Acquire the account with the smaller balance first.
Show answerHide answer
Answer: C
A common increasing key order makes both transfer directions request the same first lock, preventing the opposite acquisition pattern. Source-first locking reverses the order when the transfer direction reverses. Balance-based order can change as balances change, so it is not the fixed shared key rule.

Question 9 of 10

A loop watches descriptors 2, 8, and 14 with select. What value should it pass for nfds?
  1. 3
  2. 14
  3. 15
  4. 16
Show answerHide answer
Answer: C
Select examines descriptor numbers from 0 through nfds - 1, so the highest watched descriptor, 14, requires 15. The argument is based on the largest descriptor value, not the three entries in the watched set. Passing 14 would exclude descriptor 14 from the examined range.

Question 10 of 10

Handler A submits a read for client socket 40. Handler B then submits another read for socket 41. Both completion handlers use a global destination, now 41. A’s read completes first. Where is A’s reply directed, and what repair preserves both clients?
  1. To 40; keep using the global destination.
  2. To 41; save the destination per request.
  3. To 40; rebuild the select set before replying.
  4. To 41; keep only an outstanding-request count.
Show answerHide answer
Answer: B
A’s completion reads the overwritten global value 41, so it directs the reply to B’s client instead of socket 40. Save each destination with its own request and look it up at completion. Faster completion handling cannot make one global destination preserve two outstanding clients.
Set 2: Trace the protocol through a schedule10 Practice Questions

Save this lesson to see every answer and practice every set.

Question 1 of 10

A ring buffer has capacity 4 and starts empty with both indices at 0. It receives 21, 22, 23, removes two items, then receives 24 and 25. What is its logical removal order now?
  1. 25, 23, 24
  2. 23, 24, 25
  3. 24, 25, 23
  4. 21, 24, 25
Show answerHide answer

Save this lesson to see this answer.

Question 2 of 10

A loader sets ready under a mutex and signals before a user begins waiting. The user later locks that mutex and tests ready before deciding whether to wait. Why can it proceed?
  1. The condition variable stored the signal.
  2. Locking the mutex replays the notification.
  3. The signal passed the mutex to the later user.
  4. The ready state persists after the signal.
Show answerHide answer

Save this lesson to see this answer.

Question 3 of 10

A semaphore buffer has capacity 6 and initially contains two items. No operations are in flight. What are its empty and full permit counts?
  1. Empty 4; full 2.
  2. Empty 6; full 2.
  3. Empty 2; full 4.
  4. Empty 4; full 0.
Show answerHide answer

Save this lesson to see this answer.

Question 4 of 10

Three readers hold the basic reader-preferring lock and a writer is waiting. Two readers leave. What prevents the writer from entering yet?
  1. The writer must collect three separate write permits.
  2. The last departing reader already released writelock.
  3. The remaining reader still holds the group’s access.
  4. The counter lock stays owned by the waiting writer.
Show answerHide answer

Save this lesson to see this answer.

Question 5 of 10

A shared balance uses atomic loads and atomic subtractions. Two threads each check for at least 45 units and then subtract 45; the balance begins at 70. What flaw remains if both checks complete first?
  1. The second subtraction rejects the debit.
  2. The atomic loads reserve 45 units each.
  3. Both debits can proceed and leave −20.
  4. Only the first caller can read 70.
Show answerHide answer

Save this lesson to see this answer.

Question 6 of 10

Available is (1, 1). A has Allocation (1, 0) and Max (2, 2); B has Allocation (0, 1) and Max (1, 2). What does the banker’s safety test conclude?
  1. Safe; B can finish before A.
  2. Safe; A can finish before B.
  3. Unsafe; neither remaining claim fits initially.
  4. Unsafe; A’s full maximum exceeds Available.
Show answerHide answer

Save this lesson to see this answer.

Question 7 of 10

A timed mutex wait returns ETIMEDOUT. What does that establish?
  1. The holder is in a lock cycle.
  2. The mutex has been released.
  3. The protected state is corrupt.
  4. The wait exceeded its deadline.
Show answerHide answer

Save this lesson to see this answer.

Question 8 of 10

A server holds 30,000 mostly idle sockets and does little computation per message. Why can an event loop use less per-connection memory than one dedicated thread per socket?
  1. Connections require no state while they are idle.
  2. Connections can use small saved-state records.
  3. Each event handler receives a separate CPU core.
  4. Every socket shares one reusable network descriptor.
Show answerHide answer

Save this lesson to see this answer.

Question 9 of 10

A waiter reads done as false without the mutex. The worker then locks, sets done true, signals with no waiters, and unlocks. The waiter next locks and calls wait without rechecking. What dependency failure has occurred?
  1. A spurious wakeup before the worker’s signal.
  2. A safe Mesa recheck after mutex acquisition.
  3. A remembered signal awaiting the next caller.
  4. A lost wakeup after an unprotected check.
Show answerHide answer

Save this lesson to see this answer.

Question 10 of 10

In a single-instance wait-for graph, U → V, V → W, W → V, and X has no outgoing edge. Which threads form the dependency cycle?
  1. V and W.
  2. U, V, and W.
  3. U and X.
  4. V, W, and X.
Show answerHide answer

Save this lesson to see this answer.

Set 3: Apply the rules when conditions change10 Practice Questions

Save this lesson to see every answer and practice every set.

Question 1 of 10

A shared allocator has no free memory. One waiter needs 180 bytes and another needs 40. A release adds 90 bytes and signals only the 180-byte waiter; no other changes follow. How should the notification policy change to let an eligible waiter proceed?
  1. Broadcast after the release.
  2. Signal once 180 bytes accrue.
  3. Skip the mutex for the update.
  4. Swap both while loops for if.
Show answerHide answer

Save this lesson to see this answer.

Question 2 of 10

A capacity-two condition-variable buffer contains one item. Each of two producers checks for space under the mutex, unlocks, then later locks again only for put. Both checks finish before either put. What state can the two puts produce?
  1. Count 2; the second check reserves the slot.
  2. Count 1; one of the serialized puts is discarded.
  3. Count 3; the gap lets both checks admit an item.
  4. Count 0; the wrapped index empties the buffer.
Show answerHide answer

Save this lesson to see this answer.

Question 3 of 10

A completion semaphore has no waiting thread when its worker posts. What does that post leave for a later successful wait?
  1. One saved mutex owner.
  2. One lost notification.
  3. One blocked waiter.
  4. One stored permit.
Show answerHide answer

Save this lesson to see this answer.

Question 4 of 10

A read lock protects a cache. A reader sees a missing entry, releases read access, and waits for write access. Another writer fills the entry before it enters. What must the original reader do after acquiring write access?
  1. Insert immediately using the earlier observation.
  2. Recheck whether the entry is still missing.
  3. Retake read access while keeping write access.
  4. Release write access without examining the cache.
Show answerHide answer

Save this lesson to see this answer.

Question 5 of 10

Initialization and first use each acquire the same mutex, yet first use sometimes sees an uninitialized record. What guarantee is missing?
  1. Exclusive access during each operation.
  2. Completed initialization before use.
  3. A different private lock for each operation.
  4. A fixed delay after every mutex unlock.
Show answerHide answer

Save this lesson to see this answer.

Question 6 of 10

Tasks acquire their whole resource sets while holding a global acquisition mutex, then release that mutex before doing work. Every path follows this rule, and resource release never needs the acquisition mutex. Why can competing partial acquisitions not form a cycle?
  1. Only one incomplete acquisition sequence can exist.
  2. No thread can ever hold a resource while waiting.
  3. All resources become preemptible when work begins.
  4. The CPU cannot preempt a thread during acquisition.
Show answerHide answer

Save this lesson to see this answer.

Question 7 of 10

There are 9 identical resources. A holds 3 with Max 7; B holds 2 with Max 4. A requests 3 more. Under the banker’s algorithm, should that request be granted?
  1. No; the resulting state has no safe sequence.
  2. Yes; B can finish before A.
  3. No; the request exceeds A’s remaining claim.
  4. Yes; A can finish before B.
Show answerHide answer

Save this lesson to see this answer.

Question 8 of 10

A readable stream socket returns 0 from a successful read. What input condition should the handler process?
  1. A temporary would-block result.
  2. A received byte with value zero.
  3. End of input from the peer.
  4. A pending disk-read completion.
Show answerHide answer

Save this lesson to see this answer.

Question 9 of 10

A handler queues an asynchronous disk read, frees its destination buffer, and returns. The request is still pending. What must change before the completion path can safely use the result?
  1. Keep only the file descriptor value until return.
  2. Read immediately because submission already succeeded.
  3. Move the buffer free before request submission.
  4. Keep the buffer valid through I/O completion.
Show answerHide answer

Save this lesson to see this answer.

Question 10 of 10

One loop checks a shared quota, awaits external approval, then decrements the quota. Two tasks can each pass the check before either resumes. What change prevents both from claiming the last unit?
  1. Make quota loads atomic but keep the decision split.
  2. Lock only the final decrement after approval arrives.
  3. Reserve a quota unit atomically before awaiting.
  4. Reserve a unit after approval using the earlier check.
Show answerHide answer

Save this lesson to see this answer.

Set 4: Analyze incomplete repairs10 Practice Questions

Save this lesson to see every answer and practice every set.

Question 1 of 10

A pool starts with three permits. A borrower waits successfully, later returns its resource, and accidentally posts twice. After all work is idle, how many borrowers can pass waits before anyone posts again?
  1. 4
  2. 2
  3. 3
  4. 5
Show answerHide answer

Save this lesson to see this answer.

Question 2 of 10

A condition waiter holds mutex M for its predicate and also holds lock L. Wait releases M. The only thread that can make the predicate true must acquire L first. What stops progress, and what dependency must change?
  1. The signaler must acquire M twice before changing state.
  2. The waiter must keep M locked during the entire sleep.
  3. The waiter must discard the predicate and wait once.
  4. The waiter must stop holding L across that wait.
Show answerHide answer

Save this lesson to see this answer.

Question 3 of 10

Four consumers process a FIFO queue. All producers have now been joined, real items may remain queued, and one reserved stop marker makes one consumer exit. How many markers should the coordinator enqueue?
  1. 1
  2. 4
  3. 3
  4. 5
Show answerHide answer

Save this lesson to see this answer.

Question 4 of 10

A capacity-two semaphore buffer is empty. P1 and P2 each successfully wait on empty, then P1 inserts under the mutex and unlocks but has not posted full. P2 has not inserted. A consumer now waits on full. What happens?
  1. It removes P1’s item before P2 inserts.
  2. It blocks until P2 completes its insertion.
  3. It blocks until an item permit is published.
  4. It removes P1’s item once the mutex is free.
Show answerHide answer

Save this lesson to see this answer.

Question 5 of 10

A programmer wants a shared counter increment to be one indivisible update without using a mutex. What atomic operation directly implements that update?
  1. An atomic load only.
  2. An atomic store only.
  3. A condition signal.
  4. Fetch-and-add.
Show answerHide answer

Save this lesson to see this answer.

Question 6 of 10

Five dining philosophers use a semaphore initially set to 4. Each waits on it, picks up a left fork, posts the semaphore, then attempts the right fork. Can all five become blocked holding one fork each?
  1. No; the initial count permanently limits fork holders to four.
  2. Yes; returning permits early lets all five reach the fork cycle.
  3. No; each post atomically returns the philosopher’s left fork.
  4. Yes; but only if the initial semaphore count rises above four.
Show answerHide answer

Save this lesson to see this answer.

Question 7 of 10

Available is (1, 1). P holds (1, 0), Max (3, 1); Q holds (1, 1), Max (2, 2). P requests (0, 1). Should the banker’s algorithm grant this request?
  1. Yes; P can finish with the remaining resources.
  2. No; the tentative state has no safe sequence.
  3. Yes; Q can finish with the remaining resources.
  4. No; P asks for more than its remaining claim.
Show answerHide answer

Save this lesson to see this answer.

Question 8 of 10

A multi-instance detector sees Available (0, 0). P holds (1, 0) and requests (0, 1); Q holds (0, 1) and requests (1, 0). R holds nothing and requests (1, 0). Which holders sustain the resource deadlock?
  1. P, Q, and R.
  2. P and R.
  3. P and Q.
  4. Q and R.
Show answerHide answer

Save this lesson to see this answer.

Question 9 of 10

A select loop initially watches stream sockets 6 and 10. Only 6 is returned ready. The loop reuses that returned set without rebuilding it, while 10 receives data and reads on 6 now return 0. What two failures can follow?
  1. Socket 10 is still watched; socket 6 yields a payload zero byte.
  2. Socket 10 becomes writable; socket 6 waits for disk completion.
  3. Socket 10 is omitted; socket 6 can be redispatched at end of input.
  4. Socket 10 replaces 6 in the set; socket 6 is closed by select.
Show answerHide answer

Save this lesson to see this answer.

Question 10 of 10

Forty requests are queued on one event loop. Each handler performs a blocking disk read lasting 6 ms before returning, with negligible other work. How long does the last request wait before its read starts?
  1. 234 ms
  2. 240 ms
  3. 6 ms
  4. 39 ms
Show answerHide answer

Save this lesson to see this answer.

Set 5: Transfer the reasoning to new designs10 Practice Questions

Save this lesson to see every answer and practice every set.

Question 1 of 10

A Java thread calls notifyAll on an object outside any synchronized region and without otherwise owning its monitor. What happens?
  1. It saves a notification for later waiters.
  2. It throws IllegalMonitorStateException.
  3. It hands the caller’s locks to waiters.
  4. It waits until the monitor is free.
Show answerHide answer

Save this lesson to see this answer.

Question 2 of 10

A semaphore is implemented with a mutex, a condition variable, and a nonnegative value. Post increments and signals under the mutex. Wait checks value with if, waits if zero, then decrements. How can it still fail?
  1. The mutex lets two threads change value simultaneously.
  2. The semaphore cannot remember any post without waiters.
  3. A returning waiter can decrement a value that is zero.
  4. A post always decrements value when a waiter is absent.
Show answerHide answer

Save this lesson to see this answer.

Question 3 of 10

A consumer holds a buffer mutex while waiting on the item semaphore. The buffer is empty. A proposed repair has the producer post the item semaphore before inserting, so it can wake the consumer without the mutex. Why is that repair unsafe?
  1. It lets removal happen before the producer’s insertion.
  2. It lets insertion happen before space is reserved.
  3. It returns an empty-slot permit before removal finishes.
  4. It publishes the same completed item permit twice.
Show answerHide answer

Save this lesson to see this answer.

Question 4 of 10

In the basic reader-count design, the first reader may hold the counter lock while waiting for writelock. Why can the active writer still finish, unlike a producer blocked by a consumer’s buffer mutex?
  1. The waiting reader releases the counter lock in sem_wait.
  2. The writer already holds the reader-counter lock itself.
  3. The writer’s post waits for the reader count to reach zero.
  4. The writer can post writelock without the counter lock.
Show answerHide answer

Save this lesson to see this answer.

Question 5 of 10

A worker reads a shared pointer before the creator has finished assigning it. What kind of dependency bug is this?
  1. An order violation.
  2. A resource deadlock.
  3. Writer starvation.
  4. A retry livelock.
Show answerHide answer

Save this lesson to see this answer.

Question 6 of 10

Two update loops each acquire one lock, fail trylock on the other, release, and retry together indefinitely. A proposed fix keeps the first lock while waiting normally for the second. What does that change risk?
  1. It keeps the livelock because both threads still retry.
  2. It guarantees one finishes while the other may starve.
  3. It replaces retry livelock with a possible deadlock.
  4. It serializes both acquisitions under one common first lock.
Show answerHide answer

Save this lesson to see this answer.

Question 7 of 10

Available is (1, 1). P holds (1, 0), Max (2, 1); Q holds (0, 2), Max (1, 3); R holds (1, 0), Max (3, 2). What safe sequence can complete all three?
  1. R, P, Q.
  2. P, R, Q.
  3. Q, R, P.
  4. P, Q, R.
Show answerHide answer

Save this lesson to see this answer.

Question 8 of 10

Available is (0, 0). A holds (1, 0), currently requests (0, 0), and has remaining maximum Need (0, 2). B holds (0, 2) and currently requests (1, 0). Does current-request detection find a deadlock?
  1. Yes; neither maximum Need fits the initial Available.
  2. No; A releases (1, 0), allowing B’s request to proceed.
  3. Yes; B’s current request cannot be met immediately.
  4. No; both requests already fit the initial Available.
Show answerHide answer

Save this lesson to see this answer.

Question 9 of 10

A developer replaces repeated scans of pending I/O requests with a completion queue. What work can the loop now dispatch directly?
  1. Final responses for every newly submitted request.
  2. Follow-up work for operations reported complete.
  3. Reads on every regular file reported ready by select.
  4. Parallel execution of all handlers on one loop thread.
Show answerHide answer

Save this lesson to see this answer.

Question 10 of 10

A capacity-two ring starts empty. Put 31, put 32, get once, put 33. Both indices are now 1. What are the occupancy count and the value returned by the next get?
  1. Count 1; next value 32.
  2. Count 2; next value 33.
  3. Count 0; get must wait.
  4. Count 2; next value 32.
Show answerHide answer

Save this lesson to see this answer.

Stuck on your own question? Homework Help

Key Terms

90 Key Terms, each with its definition

A

Admission control
Admission control decides whether more work may enter a resource-limited region, delaying it when the allowed capacity is in use.
A worker waits for a processing permit before allocating its large temporary buffer.
Asynchronous I/O
Asynchronous I/O separates submitting an operation from collecting its completed result, allowing the caller to serve other work meanwhile.
The handler submits a file read and sends the response after its completion arrives.
Asynchronous I/O control block
An asynchronous I/O control block describes a POSIX AIO request, including its descriptor, file offset, buffer, and requested byte count.
The aiocb and its destination buffer remain valid while the queued read is still outstanding.
Atomic release and wait
An atomic release and wait makes releasing the mutex and entering a condition wait one indivisible handoff to competing threads.
The child cannot signal in a gap after the parent unlocks but before it starts waiting.
Atomicity violation
An atomicity violation allows another thread to interrupt accesses that must behave as a single indivisible operation.
Two separate lock regions around a balance check and debit still leave an unsafe gap.
Available vector
An Available vector counts currently unallocated units of each resource type, forming the starting Work value for a safety test.
Subtract all held units from each resource total before beginning the completion simulation.

B

Banker’s algorithm
The banker’s algorithm grants a resource request only when it fits declared remaining claims and preserves a safe completion sequence.
An allocator tentatively grants a request, tests the resulting state, and delays an unsafe grant.
Binary semaphore
A binary semaphore used as a lock starts with one permit and pairs acquisition before a critical section with release afterward.
A second entrant blocks until the first returns the critical section’s permit.
Bounded buffer
A bounded buffer is a shared queue with fixed capacity that makes producers wait when full and consumers wait when empty.
A capacity-three buffer cannot accept a fourth item until a consumer removes one.
Broadcast
A broadcast wakes all current waiters on a condition variable; each must reacquire the mutex and test its own predicate.
An allocator broadcasts after a release because waiters request different amounts of memory.
Buffer mutex
A buffer mutex serializes changes to the buffer array and indices, independently of semaphores that count available resources.
Two producers with valid space permits still update the buffer’s write index, fill_ptr, under the same mutex.

C

Check-then-act
A check-then-act operation tests shared state and then changes or uses it in a way that relies on that test remaining valid.
Testing stock and reserving it must stay together when other threads can sell the same item.
Circular wait
A circular wait is a closed chain of threads in which each waits for a resource held by the next thread in the chain.
Three threads waiting A-to-B, B-to-C, and C-to-A close the dependency cycle.
Compare-and-swap
Compare-and-swap atomically replaces a location’s value only when its current value matches the expected value supplied by the caller.
A counter updater retries when another thread changes the old value before its CAS succeeds.
Completion queue
A completion queue delivers records for finished asynchronous operations, allowing a program to collect results and dispatch follow-up work.
The loop reads a completion record instead of repeatedly checking every outstanding read.
Condition variable
A condition variable provides a queue where threads sleep until a change in shared state lets them try their condition again.
A consumer waits on a condition variable when the request queue contains no work.
Continuation
A continuation stores the state needed to resume a task after a handler has returned while waiting for an asynchronous operation.
The read request saves which client socket should receive its data.
Counting semaphore
A counting semaphore starts with the number of available identical units and limits how many users can acquire them at once.
A connection pool with four permits makes a fifth borrower wait for a returned connection.
Covering condition
A covering condition uses a broad notification to reach every waiter whose more specific condition might now be satisfied.
When an allocator with waiting 10-byte and 100-byte requesters releases 50 bytes, it wakes both so each can recheck.

D

Deadlock
A deadlock traps a set of blocked threads because each needs an action or resource that only another trapped member can provide.
T1 holds A and waits for B while T2 holds B and waits for A.
Deadlock avoidance
Deadlock avoidance checks proposed resource grants against advance demand information and delays grants that would leave the system unsafe.
A free resource may be withheld when granting it would remove every safe completion sequence.
Deadlock detection
Deadlock detection searches current waiting dependencies or resource requests for an existing set that cannot make progress.
A cycle search finds three mutex holders blocked on one another.
Deadlock prevention
Deadlock prevention designs resource acquisition so at least one necessary deadlock condition can never arise.
A common account-lock order prevents reverse-direction transfers from creating circular wait.
Descriptor set
A descriptor set represents file descriptor numbers selected for monitoring, with bits indicating membership or returned readiness.
The server rebuilds its read set because select() replaces it with the ready subset.

E

End-of-data marker
An end-of-data marker is a reserved queue item that tells a consumer to stop after all preceding real items have been handled.
A coordinator joins every producer before inserting one stop marker per consumer.
epoll
epoll is a Linux event interface that keeps an interest set in the kernel and reports ready events for registered descriptors.
A server can monitor many connections without resubmitting the full watched list each time.
Event loop
An event loop waits for events and dispatches short handlers, letting one thread coordinate many tasks whose waiting periods overlap.
A server handles data from one ready socket, then returns to serve another connection.

F

Fair admission
Fair admission grants access according to a policy that prevents a continuing class of arrivals from indefinitely bypassing a waiter.
A fair readers–writers policy prevents later readers from endlessly overtaking an earlier writer.
Fetch-and-add
Fetch-and-add is an atomic read-modify-write operation that adds an amount and returns the old value without an intervening update.
An atomic fetch-and-add keeps concurrent counter increments from overwriting each other.

H

Hoare semantics
Hoare semantics transfer the lock and execution directly to a notified waiter, preserving the signaled condition at that handoff.
The notified thread resumes before another entrant can consume the newly available item.
Hold-and-wait
Hold-and-wait occurs when a thread keeps resources it already holds while blocking for additional resources it needs.
A transfer keeps one account lock while waiting to acquire the other account lock.
Hybrid concurrency
Hybrid concurrency combines an event loop with workers that perform blocking or CPU-heavy operations away from the loop thread.
A network loop sends image processing to workers and later handles their completions.

I

Implicit blocking
Implicit blocking delays a thread through an operation that looks immediate, such as accessing a page that must be read from disk.
A handler stalls on a page fault even though it makes no explicit blocking read call.
Interleaving
An interleaving is an ordering of steps from different threads that preserves each thread’s own order while mixing their accesses.
Placing a pointer clear between another thread’s check and use exposes the unsafe schedule.
io_uring
io_uring is a Linux interface with submission and completion queues for asynchronous operations, supporting completion-driven I/O handling.
A server submits requests and later dispatches continuations from their completion records.
Item semaphore
An item semaphore counts published buffer items that consumers can reserve before removing them from the shared array.
An initially empty buffer gives its item semaphore zero permits.

J

Join
A join waits for a particular thread to terminate before the joining thread continues with work that needs its completion.
The reporting thread joins the summing worker before reading its final total.

K

kqueue
kqueue is an event-notification interface on FreeBSD and macOS that maintains registered interests and returns reported events.
A server registers socket interests with kqueue instead of rebuilding full select descriptor sets.

L

Livelock
A livelock keeps threads running and responding to one another while their repeated actions fail to complete useful work.
Two trylock loops repeatedly release and retry together without either obtaining both locks.
Lock ordering
Lock ordering assigns a shared acquisition order to locks and requires every relevant code path to follow that order.
Both directions of a transfer lock the lower-numbered account before the higher-numbered one.
Lock upgrade
A lock upgrade changes read access to exclusive write access; in the basic design, read access must first be released.
After reacquiring write access, a cache updater rechecks whether the entry still needs changing.
Lost update
A lost update occurs when overlapping read-modify-write operations overwrite one another, so an intended change disappears.
Two threads load count 8 and each store 9, although two increments should leave 10.
Lost wakeup
A lost wakeup occurs when a needed notification passes before a thread begins waiting, leaving it asleep without a future notification.
When a parent checks a completion flag outside the mutex, the child can finish between the check and the wait.

M

Manual stack management
Manual stack management explicitly preserves local task state across returning handlers instead of relying on one suspended call stack.
A response record keeps the request position and destination until the read finishes.
Mesa semantics
Mesa semantics make a notified waiter ready to compete for its lock, so it must recheck the condition after reacquiring it.
A second consumer can take the only item before the notified first consumer gets the mutex back.
Monitor
A monitor groups shared state with operations that allow one thread at a time, together with condition-based waiting.
Java’s synchronized block holds the object monitor required for calls to wait().
Multicore parallelism
Multicore parallelism runs computation on several CPU cores at the same time, requiring more execution workers than one loop thread.
Several image-processing workers can compute at once while one event loop handles sockets.
Mutual exclusion
Mutual exclusion allows only one thread at a time to hold a resource or execute a protected operation on shared state.
An exclusive mutex cannot be granted to a second thread while its owner still holds it.

N

Need vector
A Need vector records a thread’s remaining maximum claim for each resource type, computed as Max minus current Allocation.
A thread with Max (4, 3) and Allocation (1, 2) has Need (3, 1).
No preemption
No preemption means a held resource cannot be forcibly reclaimed from its holder and must be released through its protocol.
An ordinary mutex waiter cannot simply take the lock away from its current owner.
Non-blocking I/O
Non-blocking I/O returns control when an operation cannot proceed immediately, so the calling thread can handle other work.
A socket read returns EAGAIN and the handler returns to the event loop.

O

Order violation
An order violation occurs when an operation executes before a required earlier operation because their dependency is unenforced.
A worker reads configuration before its loader has finished publishing the values.

P

Permit
A permit represents one available admission or resource unit that a successful semaphore wait consumes and a post supplies.
A semaphore with three unused permits can admit three threads without blocking them.
poll
poll() monitors an array of descriptor entries for requested I/O events without the fixed descriptor-value restriction of fd_set.
A high-numbered socket can be watched with poll even when its value will not fit a select set.
Predicate
A predicate is a true-or-false condition on shared state that a thread checks before deciding whether it can proceed.
The predicate done != 0 lets a parent test whether its child has finished.
Producer–consumer problem
The producer–consumer problem coordinates threads adding and removing items from a shared buffer with limited capacity.
A network receiver produces requests that worker threads consume from a bounded queue.

R

Random backoff
Random backoff adds a randomly chosen delay before retrying, reducing repeated collisions between otherwise synchronized contenders.
Retrying transfers wait different intervals so they are less likely to collide again.
Reader preference
Reader preference lets arriving readers join an active reader group even when a writer is already waiting for access.
Overlapping readers can keep the reader count above zero while a writer waits.
Readers–writers lock
A readers–writers lock allows several readers to share access or one writer to hold exclusive access to protected data.
Several cache lookups can overlap, but updating the cache requires write access.
Readiness
Readiness reports that an I/O operation can be attempted for a descriptor; it does not transfer the data into the program.
A readable socket can return payload data or report end of input when read.
Readiness flag
A readiness flag records completed initialization under synchronization, allowing users to wait until the required state exists.
The creator sets ready only after assigning the record the worker will use.
Recovery victim
A recovery victim is selected work that a system aborts or rolls back to release resources and break a detected deadlock.
A process with little completed work may be cheaper to restart than a nearly finished one.
Rendezvous
A rendezvous is a meeting point that neither of two threads may pass until both have announced that they arrived.
Each thread posts its arrival before waiting for the other thread’s arrival permit.
Resource reservation
A resource reservation assigns a resource unit to one admitted operation, even before that operation updates shared bookkeeping.
A producer’s successful space wait reserves a slot while it waits for the buffer mutex.
Ring buffer
A ring buffer stores a queue in an array whose read and write indices wrap from the final slot back to the first.
After writing slot 2 of a three-slot array, the next write position wraps to slot 0.
Rollback
A rollback restores an earlier valid checkpoint so work can release or reclaim resources without preserving partial changes.
A transaction system aborts and retries a deadlocked transaction through its rollback mechanism.

S

Safe sequence
A safe sequence orders all threads so each remaining maximum claim fits the resources available after earlier threads finish.
Each step of a proposed sequence must fit every resource type, not just the total count.
Safe state
A safe state has at least one completion order that can satisfy every thread’s remaining maximum demand using available and released resources.
One thread can finish first and release enough resources for a second thread to complete next.
select
select() monitors descriptor sets for I/O readiness, replacing them with ready subsets and limiting examination to descriptor numbers below nfds.
Watching descriptors 5 and 12 requires nfds 13 and rebuilding the watched sets after each call.
Semaphore
A semaphore is an integer-based synchronization object with atomic wait and post operations that acquire and supply permits.
A post can occur before a wait because the semaphore remembers the unused permit.
Semaphore ownership
A semaphore has no owner, so any thread can post a permit, even when a different thread performed the corresponding wait.
A child posts a completion semaphore that its parent waits on.
Semaphore post
A semaphore post atomically supplies one permit, allowing a blocked waiter to continue or saving availability for a later wait.
A worker posts its completion semaphore even if its parent has not started waiting yet.
Semaphore wait
A semaphore wait atomically acquires one permit, blocking when no permit is available until another operation supplies one.
A borrower waits before taking a handle so it cannot exceed the pool’s admission limit.
Signal
A signal notifies at least one current waiter on a condition variable when waiters exist, without storing a notification for later arrivals.
A child signals after setting its protected completion flag so a waiting parent can recheck it.
Signed-counter model
The signed-counter model represents blocked semaphore waiters by negative values while preserving the same thread-visible wait/post behavior.
A trace value of −2 represents two blocked waiters, not a negative Linux permit count.
Space semaphore
A space semaphore counts permits to reserve currently available buffer slots before producers insert their completed items.
An empty capacity-five buffer initializes its space semaphore to five.
Spurious wakeup
A spurious wakeup is a return from a condition wait without a matching notification, requiring the same predicate recheck as any wakeup.
A consumer loops back into wait if it wakes but still finds the buffer empty.
Starvation
Starvation postpones one thread indefinitely while others continue useful work, even though no blocked dependency cycle traps them all.
A writer keeps waiting while an unbroken stream of overlapping readers uses the resource.
State variable
A state variable records a fact about shared work, so a thread can test it even after an earlier notification has passed.
A completion flag remains set when the child finishes before the parent starts waiting.
Suspension
A suspension pauses one function until an awaited operation finishes while allowing the event loop to run other ready work.
An async function resumes after its read completes, with its local variables preserved.

T

Throttling
Throttling limits concurrent entry to an expensive phase so the combined demand stays within a chosen resource budget.
Twelve simultaneous 500 MB workers use 6,000 MB during their memory-heavy phase.
Timeout
A timeout ends a wait after a deadline, revealing excessive delay without distinguishing a permanent deadlock from slow progress.
An expired timed mutex wait can result from an overloaded system as well as a lock cycle.
Trylock
A trylock attempts to acquire a mutex and returns immediately when it is unavailable, allowing the caller to release and retry.
A failed second trylock makes the transfer release its first account lock.

U

Unsafe state
An unsafe state has no guaranteed maximum-demand completion sequence, although actual future behavior may still avoid deadlock.
A thread might release resources early instead of requesting its entire remaining claim.

W

Wait-for graph
A wait-for graph draws threads as nodes and waiting dependencies as directed edges from the waiter to a resource holder.
An arrow from T1 to T2 means T1 needs a resource that T2 currently holds.
Watchdog timer
A watchdog timer triggers recovery when software fails to report progress within a deadline, without uniquely diagnosing the cause.
An embedded controller resets after its software stops sending the expected progress reports.
Work vector
A Work vector is the safety test’s simulated availability, increased by a thread’s Allocation whenever that thread is marked finished.
Finishing a thread holding (2, 1) changes Work (1, 3) to (3, 4).
Worker pool
A worker pool is a bounded group of reusable threads or processes that perform submitted tasks and return their results.
libuv dispatches file system operations to its thread pool while the loop serves events.
Writer preference
Writer preference blocks new readers when a writer is waiting, reducing writer postponement but potentially starving readers.
An arriving lookup waits behind the cache writer instead of joining the current reader group.
Writer starvation
Writer starvation is indefinite postponement of a waiting writer while readers continue entering and making progress.
A lock that only counts readers gives a writer no turn if new readers continually overlap.

About this lesson

By StudeliReport a mistake

Make your own Study Materials

Turn your notes, slides and documents into Study Guides, Practice Questions, Flashcards and 6 more tools.

Get Started