CSE 513
CSE 513 · WEEK 03 · READING

Concurrency and synchronization

How can concurrent activities cooperate without corrupting shared state?

Select figures for full-size views. Predict each interleaving before opening a worked answer.

1. Shared state and the limits of scheduling

A scheduler chooses which ready thread receives CPU time. It does not know that two fields represent a bank balance, that an index must remain inside a queue, or that a customer identifier must be reserved before it is handed out. Those are rules of the program. When threads share mutable state, the program must make the rules survive every execution order that its synchronization permits. This connects Week 2’s scheduling decisions to Week 3’s central question: how can concurrent activities cooperate without corrupting shared state?

Concurrency does not require several processors. On one processor, a thread can be preempted halfway through a larger operation; another thread then observes or changes the same object. On several processors, operations may also run simultaneously. The correctness issue is the same: another participant can encounter an intermediate state that the programmer did not intend to expose. Increasing a time slice, adding a delay, or observing that the program usually finishes successfully does not define a protection protocol.

Threads share their process’s address space, including globals and allocated objects, but retain their own execution state and stacks. A variable declared locally can still be a pointer to an object shared with another thread. Begin an analysis by naming the objects, their readers and writers, and the rules that valid state must satisfy. This is more useful than asking whether the whole program is “thread-safe” without saying what that term means for its operations.

2. A lost update, step by step

Imagine two workers, each asked to increment a shared counter once. The intended result from zero is two. In a deliberately simple model, an increment consists of a read into a private register followed by a write of that register plus one. Assume each read and each write is atomic, and that all threads see an execution equivalent to one interleaving preserving each thread’s program order. These assumptions define the model; they are not permission to write unsynchronized ordinary C accesses.

A legal interleaving in the abstract model that loses one increment
StepThread AThread BShared counter
0Not startedNot started0
1Read 00
2Read 00
3Write 11
4Write 11

The arithmetic in both workers is correct: zero plus one is one. The error is that both workers base their update on the same old value. When B writes, it does not add to the current shared counter; it replaces that counter with a result already calculated from B’s private copy. A successful schedule is also possible: A reads and writes before B reads, so B obtains one and writes two. The existence of the successful schedule does not remove the failing one.

With two ordered steps per worker there are six possible interleavings. AABB and BBAA finish with two; ABAB, ABBA, BAAB and BABA finish with one. The letters identify whose next step runs. These schedules are not equally likely samples from a real operating system, so the list establishes possible outcomes in this model, not a two-thirds failure probability. Reasoning about a protocol is different from measuring a scheduler.

Predict: would making the final write atomic solve the lost update?

No. The model already gives each write atomicity. The required indivisible operation includes reading the old value and calculating its replacement. Protect the complete read–modify–write operation with a mutex, or use an appropriate atomic read–modify–write primitive.

Two processes request and receive the same conceptual next-available process identifier.
The same check-and-reserve problem appears in resource allocation. This figure is an illustrative failure scenario, not a claim that contemporary Linux fork() returns duplicate live process identifiers. Retained from Silberschatz, Galvin and Gagne, Operating System Concepts, 10th edition, Chapter 6. Select the image for the full-size view.

3. The model and executable C are different

A race condition is a correctness failure whose occurrence depends on timing or ordering. A C data race has a more specific language meaning: conflicting accesses in different threads, at least one non-atomic, without the required happens-before relationship cause undefined behaviour. A data-racy C program therefore cannot be explained completely by promising that the counter will merely be too small. Compiler transformations and the language’s rules matter as well as the machine instruction sequence.

The qualifier volatile is not a mutex and does not establish the synchronization required between threads. Nor is a pause a proof that another thread has reached a particular point. For teaching we can use the abstract trace above, then use a well-defined executable experiment. The supplied counter program deliberately creates a lost update using separate atomic loads and stores. A condition-variable rendezvous ensures both threads have loaded zero before either stores one. There is no C data race, but the compound increment is still wrong.

Contrast that experiment with atomic_fetch_add. It requests one atomic read–modify–write operation. A mutex also works, by admitting only one worker at a time to the region containing the whole increment. These two correct solutions need not have the same performance or generality: a mutex can protect a multi-field invariant, whereas one atomic numeric operation solves a narrower problem. See the C11 committee draft, §5.1.2.4 for the language-level definition.

4. Invariants and critical sections

An invariant describes the valid states of an object. For a queue of capacity N, one useful invariant is 0 ≤ count ≤ N. A stronger specification includes “each accepted item is removed exactly once” and, for a FIFO queue, “items are removed in insertion order.” For a transfer between accounts, the combined balance is conserved even while each individual balance changes. The synchronization design must protect the state transitions required by these statements.

A critical section is the part of an operation that accesses protected shared state under an exclusion protocol. Entry requests permission, exit releases it, and independent remainder work happens outside. A lock should surround the complete transition that needs to appear indivisible to other participants. Protecting only the final assignment can leave the earlier decision exposed; protecting a writer while allowing an unsynchronized reader can expose a half-completed update.

Loop containing entry section, critical section, exit section and remainder section.
Use the original structural diagram to identify exactly where permission is acquired and returned. Retained from Silberschatz, Galvin and Gagne, Operating System Concepts, 10th edition, Chapter 6. Select the image for the full-size view.

The textbook separates three requirements. Mutual exclusion prevents simultaneous conflicting critical sections. Progress prevents indefinite postponement of choosing an entrant when the section is free and someone wants it. Bounded waiting limits how many other entries may occur after a particular request. The last requirement is a fairness property, not a consequence of exclusion. A design that locks everyone out forever is safe against simultaneous entry but fails to provide useful service.

5. The gas-station bathroom key

One restroom key attached to a long wooden stick hangs on a single hook while two customers wait near a closed restroom.
The instructor’s gas-station analogy makes ownership and return of a single mutex visible. AI-generated classroom illustration based on the instructor’s analogy; not a photograph of the instructor’s equipment. Select the image for the full-size view.

Consider a single-occupancy restroom whose key hangs in the gas-station office. The key is attached to a long stick so that it is conspicuous and inconvenient to leave in a pocket. A customer takes the one key, uses it to open the restroom, keeps it throughout the visit, and returns it after leaving. In the model, the restroom is the protected resource, the customer is a thread, taking the key is lock acquisition, and returning it is unlock. The large attachment encourages correct return; it does not implement synchronization by itself.

The important rule is that taking the key is indivisible. Two customers must not both observe “the key is available” and then each proceed as an owner. In software, “check unlocked” followed later by “mark locked” has exactly that gap. An atomic acquisition operation removes it. Once acquired, possession represents ownership: another customer must wait, and the owner returns the key after restoring the resource to an available state.

The analogy has assumptions. There is one usable key, no alternative entrance, everyone follows the procedure, and the owner does not return the key before leaving. The hook is not a fair queue: someone who repeatedly reaches it first could delay another customer. Losing the key represents a missing release; returning it early allows another entrant too soon. A spare key or someone bypassing the office represents an access path that ignores the synchronization protocol. The physical lock on a door and a software mutex are different mechanisms, but the ownership discipline is comparable.

Discuss: can a friend return the mutex on behalf of its owner?

For an ordinary POSIX mutex, the program must respect ownership and mutex-type rules; treat unlocking as the acquiring thread’s responsibility. A counting semaphore has different semantics and can be posted by a different participant. The analogy must not erase that distinction.

6. Software algorithms and their assumptions

Strict alternation uses a shared turn variable: only the thread whose number equals turn can enter, and it hands over the turn on exit. This ensures exclusion in the simple model but can violate progress. If thread 1 is doing unrelated work and does not want to enter, thread 0 should not be forced to wait solely because it is thread 1’s turn. A protocol must distinguish interest in entry from the tie-breaking rule used when both are interested.

Peterson’s two-thread algorithm adds one interest flag per thread. A thread announces interest, then gives the other thread priority in a tie. With atomic reads and writes and sequential consistency, if both flags are true, the last write to turn makes one thread wait. The other can enter and later clear its flag. Both cannot simultaneously win the tie while both flags remain true, since the single turn value cannot favor both at once.

/* Abstract pseudocode: j is the other thread. */
flag[i] = true;
turn = j;
while (flag[j] && turn == j) { }
/* critical section */
flag[i] = false;
Two process timelines illustrate that reordering an interest-flag update can permit overlapping critical sections.
The original figure shows why the ordering assumptions of the proof matter. Retained from Silberschatz, Galvin and Gagne, Operating System Concepts, 10th edition, Chapter 6. Select the image for the full-size view.

The pseudocode is not a portable C implementation with plain shared variables. The abstract proof assumes memory behaviour that a compiler and processor do not automatically provide for that program. Production application code should use established synchronization interfaces or a carefully specified atomic algorithm. It is misleading to teach a generic “memory barrier” as a magic operation that makes all memory immediately consistent everywhere: barriers and atomic operations provide particular ordering guarantees, with different strengths and contexts.

7. Atomic primitives and mutex implementation

Hardware-supported read–modify–write operations provide building blocks for synchronization. Test-and-set changes a flag while returning its prior state. Compare-and-swap conditionally replaces a value if it matches an expected value. If a lock is initially zero and two threads attempt to change zero to one atomically, only one can succeed before the lock is released. The loser cannot legitimately infer ownership merely because it attempted the operation.

Atomic lock acquisition is only part of a usable lock. The implementation must also provide the memory ordering needed for protected accesses and decide what a losing contender does. A simple spin loop consumes execution time while repeatedly testing. A blocking mutex can put a waiter to sleep. Spinning may be useful for very short waits when the owner can run concurrently; blocking avoids consuming a processor for a long wait. Actual implementations may combine these approaches, so “mutex” does not mean “always busy-waits.”

Turning off interrupts on one processor does not stop a second processor from accessing memory. Kernel code also has execution-context restrictions that ordinary application code does not share. Current Linux lock rules depend on lock type and configuration, including PREEMPT_RT. Treat the textbook’s platform snapshots as background, not as universal contemporary implementation claims. Our practice uses user-space pthreads; see the Linux lock-type documentation for kernel-specific distinctions.

8. Using a mutex correctly

POSIX source snippet declares a pthread mutex and initializes it.
Initialize the object before threads start using it; check errors in a complete program. Retained from Silberschatz, Galvin and Gagne, Operating System Concepts, 10th edition, Chapter 7. Select the image for the full-size view.
POSIX mutex lock and unlock enclose a critical section.
Every relevant participant must use the same protection protocol. Retained from Silberschatz, Galvin and Gagne, Operating System Concepts, 10th edition, Chapter 7. Select the image for the full-size view.

For the counter, lock before incrementing and unlock afterward. For a transfer, protect both the debit and credit if other operations need a consistent combined view. An invariant temporarily broken inside the critical section must be restored before release. Readers that need a consistent snapshot participate in the protocol too. An object is not protected simply because a lock happens to exist somewhere in the program.

Choose scope deliberately. A single coarse lock often simplifies reasoning. Several locks may permit more concurrency, but introduce questions about which fields each protects and in which order they can be acquired. Independent computation belongs outside the critical section. Long I/O or waiting for a different resource while holding a mutex may delay all contenders or create dependencies that need separate analysis. Those dependencies lead naturally to Week 4’s deadlock discussion.

Initialize once before use and destroy only after every user has finished. The practice code checks pthread return codes, which are returned as error numbers rather than reported through errno in the same way as many other C library calls. It uses ordinary initialized mutexes, not robust-mutex recovery or thread cancellation. Those are additional protocols with their own rules, outside this exercise’s assumptions.

9. Magic Crossroads and semaphore permits

A figure-eight tin toy roadway with a red car in the shared crossing and a blue car held before it by a small mechanical latch.
Magic Crossroads illustrates waiting before a shared crossing. The latch depiction is conceptual; the image does not reconstruct the hidden mechanical linkage. Toy appearance reference: James and Jess’ House of Goods product photographs. AI-generated classroom illustration based on the instructor’s analogy; not a photograph of the instructor’s equipment. Select the image for the full-size view.

The instructor brings a Magic Crossroads-style wind-up toy into class. Its roads cross at the same level, so cars on conflicting paths cannot safely occupy the crossing together. As demonstrated by the instructor, a small mechanical latch stops one car just before the intersection. The car is held outside the shared region while another can pass. This gives a concrete model of a binary semaphore guarding a critical section: the crossing is the shared resource, and the latch makes waiting visible.

Model the crossing with one permit. A successful wait reserves it before a car enters; the permit is returned only after the car has cleared the crossing. A conflicting car must remain behind its latch until it can acquire the permit. Checking that the crossing looks empty and then entering are not two independently competing actions: admission must be atomic. The safety invariant is at most one car in the shared crossing. Releasing too early admits a collision; never releasing prevents progress. This is the software model we draw from the demonstration, not a claim about the toy’s exact internal linkage.

Winding the spring supplies energy, not permission to cross. A wound car can still be held at the latch. In software, a thread blocked on a semaphore may sleep rather than consume a processor while waiting; the mechanical toy does not reproduce CPU scheduling. Nor does this demonstration establish FIFO admission or starvation freedom. Observe which car waits and when it is released, then separate that observation from guarantees made by a software interface.

A counting semaphore generalizes permit accounting to several interchangeable resources. With S = 2, A and B can acquire while C waits; a release allows another acquisition. This capacity-two example is separate from the toy’s single shared crossing: increasing its admission count to two would defeat exclusion. The bathroom key emphasizes mutex ownership; the crossroads latch emphasizes waiting and controlled admission. A semaphore does not intrinsically identify an owner. In a producer–consumer design, one participant can post a permit that a different participant consumes. A semaphore initialized to zero can also enforce order: initialization posts ready, and use waits for ready. Permits persist when no one is waiting, unlike condition-variable notifications.

10. POSIX semaphores and common mistakes

An unnamed POSIX semaphore is initialized with process-sharing argument zero and initial value one.
The zero sharing argument selects use by threads in one process. Retained from Silberschatz, Galvin and Gagne, Operating System Concepts, 10th edition, Chapter 7. Select the image for the full-size view.
sem_wait and sem_post surround use of a resource.
Check acquisition success before treating the resource as held. Retained from Silberschatz, Galvin and Gagne, Operating System Concepts, 10th edition, Chapter 7. Select the image for the full-size view.

An unnamed semaphore can also be process-shared when its sharing argument is nonzero and the semaphore resides in memory actually shared by the processes. Named semaphores provide a different way to find a semaphore through a name. The distinction is not simply “named for processes, unnamed for threads.” The sem_init manual specifies the storage requirement.

Check semaphore errors. A blocking sem_wait may be interrupted; applications often retry EINTR when that is consistent with their cancellation or termination policy. Do not post after a failed acquisition as if it had succeeded, because that creates a permit without a matching consumption. Similarly, posting twice after one acquisition corrupts resource accounting even though each individual post is atomic. See sem_wait for return and error semantics.

Predict: a semaphore protecting three tools accidentally receives two posts for one return. What fails?

Its count now overstates available resources. The semaphore implementation can remain correct while the application admits more users than real tools. The protocol must preserve the correspondence between permits and resources.

11. Monitors, predicates and condition variables

A monitor encapsulates shared data and operations, with an entry queue outside.
Encapsulation makes the protection boundary visible. Retained from Silberschatz, Galvin and Gagne, Operating System Concepts, 10th edition, Chapter 6. Select the image for the full-size view.

A monitor groups shared state with operations that execute under mutual exclusion. A bounded queue can expose put and get without exposing its internal count or array. Entry to the monitor ensures only one operation manipulates that state at a time. But exclusion alone cannot solve every problem: a consumer may enter safely and discover that there is no item to consume. It needs to wait for a predicate, not merely for the mutex to become free.

A monitor has an entry queue and separate queues associated with condition variables.
Waiting for admission and waiting for a predicate are different situations. Retained from Silberschatz, Galvin and Gagne, Operating System Concepts, 10th edition, Chapter 6. Select the image for the full-size view.

For a consumer the predicate might be count > 0; for a producer it might be count < capacity. The condition variable does not contain that truth value. The application fields do. A waiting thread holds the associated mutex while testing the predicate, then uses condition wait to release the mutex and wait as one coordinated operation. Another thread can then enter, change the state, and notify. On return from wait, the waiter owns the mutex again and must recheck.

A POSIX thread holds a mutex and waits in a while loop until a equals b.
The while loop is essential under the POSIX condition-wait semantics. Retained from Silberschatz, Galvin and Gagne, Operating System Concepts, 10th edition, Chapter 7. Select the image for the full-size view.
A POSIX thread changes a to equal b, signals the condition variable and unlocks.
Update the shared predicate under the same protection protocol. Retained from Silberschatz, Galvin and Gagne, Operating System Concepts, 10th edition, Chapter 7. Select the image for the full-size view.

A notification is not a reservation. If one consumer is awakened, another consumer may obtain the mutex first and remove the available item. A spurious wakeup is also permitted. In both cases a returning waiter may find the predicate false. Replacing while with if can allow it to continue into an invalid operation. The POSIX condition-wait specification explains the release-and-wait behaviour and required predicate re-evaluation.

A lost wakeup occurs when the predicate check and decision to wait are separated incorrectly: a consumer checks empty without the required mutex, the producer inserts and signals, and then the consumer starts waiting. There is no saved condition-variable permit to collect. The correct protocol protects the predicate and uses the atomic release-and-wait operation. Notifications help waiters notice change; the persistent application state determines whether they should sleep at all.

A Java object lock lies between its entry set and wait set.
This original illustration separates waiting to acquire a lock from waiting after releasing it. It is a conceptual comparison, not a Java practice assignment. Retained from Silberschatz, Galvin and Gagne, Operating System Concepts, 10th edition, Chapter 7. Select the image for the full-size view.

12. Truck, conveyor and bounded storage

A worker unloads watermelons from a truck onto a conveyor while another removes them into crates.
The conveyor is finite storage between a producer and consumer; the workers need both exclusion and coordination. AI-generated classroom illustration based on the instructor’s analogy; not a photograph of the instructor’s equipment. Select the image for the full-size view.

The truck supplies melons, a loading worker places them onto a conveyor, and an unloading worker removes them at the other end. The loader is a producer, the unloader a consumer, and the conveyor’s carrying positions form a bounded buffer. The physical belt provides an intuitive FIFO order: a melon reaches the unloading end after those placed ahead of it. Software implements that order with indices and a storage array, so it must preserve it explicitly.

If the conveyor is full, the loader must wait instead of placing a melon on an occupied position. If it is empty, the unloader must wait instead of removing nonexistent produce. Faster loading does not justify overwriting a melon; faster unloading does not justify inventing one. These are safety rules. The liveness question is whether each worker can eventually proceed when the other makes the needed space or item available.

For N slots, use empty = N, full = 0 and a mutex protecting the queue’s internal state. The producer reserves empty space, takes the mutex, inserts, releases the mutex and posts full. The consumer reserves a full slot, takes the mutex, removes, releases and posts empty. Do not hold the queue mutex while waiting for the item or space that the other participant must create; otherwise that participant may be unable to enter the queue operation.

producer: wait(empty); lock(m); insert(item); unlock(m); post(full);
consumer: wait(full);  lock(m); remove(item); unlock(m); post(empty);

At completed-operation boundaries with no outstanding reservations, empty plus full equals N. During an operation, a worker may hold a permit that is no longer available but has not yet become a published item or free slot. Therefore blindly equating semaphore values with physical occupancy at every instruction is incorrect. Explain which point of the operation your trace describes.

A capacity-two conveyor at quiescent operation boundaries
Completed operationQueueemptyfull
Start[]20
Produce A[A]11
Produce B[A, B]02
Consume A[B]11
Produce C[B, C]02
Diagnose: a consumer locks the buffer and then waits for full while the queue is empty.

The producer needs that same mutex to insert an item before posting full. The consumer holds the mutex while waiting for the producer, so neither can complete the required next step. Reserve the item before acquiring the queue mutex, or use a condition wait that releases it.

13. A queue with condition variables and closure

The alternative in the supplied C program uses a mutex, not_empty and not_full. A producer waits while count equals capacity; a consumer waits while count is zero. Each condition wait releases the mutex and reacquires it before returning. After insertion the producer signals not_empty, and after removal the consumer signals not_full. The count, indices, array and closure flag all follow the same mutex discipline.

Finite work needs a completion protocol. The producer inserts integers 1 through 10,000, then sets closed under the mutex and broadcasts not_empty. The consumer waits only while empty and not closed. If closed but not empty, it continues draining. If closed and empty, it terminates. Treating any closure as an immediate exit would silently discard items still waiting on the conveyor.

The code has one producer and one consumer. It checks each received integer against the next expected FIFO value, then records the number consumed and the sum. Main joins both threads before reading those results. The expected sum is 10,000 × 10,001 / 2 = 50,005,000. Multiple producers require a revised definition of when the final producer closes the queue; several consumers require suitably designed work accounting. Do not extend the number of threads without revisiting those assumptions.

14. Dining philosophers and global progress

Five philosophers around a circular table share five utensils, one between each neighboring pair.
A philosopher needs both neighboring utensils; exclusive ownership of each alone does not guarantee progress. AI-generated classroom illustration based on the instructor’s analogy; not a photograph of the instructor’s equipment. Select the image for the full-size view.
Original dining-philosophers table with five seats and shared chopsticks around a rice bowl.
The textbook’s familiar diagram is retained alongside the new classroom illustration. Retained from Silberschatz, Galvin and Gagne, Operating System Concepts, 10th edition, Chapter 7. Select the image for the full-size view.

Five philosophers alternate thinking and eating. Each needs the two adjacent chopsticks, and each chopstick has only one owner at a time. Let every philosopher first acquire the left chopstick and then wait for the right. One possible schedule lets all five acquire their left chopstick before anyone acquires a second. Each right chopstick is now held by a neighbor who is also waiting. No philosopher reaches the release step.

This execution preserves mutual exclusion perfectly: no chopstick is shared by two owners. Its failure is global progress. A locally correct lock does not automatically make a collection of acquisitions correct. The same pattern appears when application threads need two or more locks. Week 4 will express these dependencies as resource and wait-for graphs and study deadlock systematically.

One remedy imposes a common order. Number chopsticks 0 through 4 and require every philosopher to acquire the lower-numbered needed chopstick first. Any chain of waiting for a second chopstick then follows increasing numbers. A cycle would require those numbers eventually to increase back to a smaller starting number, which is impossible. This breaks the circular acquisition pattern, provided all participants obey the rule. It does not establish that every hungry philosopher receives service within a bounded time.

A monitor solution records THINKING, HUNGRY and EATING states under one lock. It grants eating only when neither neighbor is eating; waiting releases the monitor lock so others can finish and change state. The eating activity takes place after permission has been recorded, not while monopolizing the monitor’s internal lock. The textbook protocol avoids deadlock but can still require additional fairness policy to rule out starvation. Distinguish the permission state from ownership of the monitor lock itself.

15. Fairness, readers–writers and priority inversion

Readers–writers is another useful policy problem. Several readers can inspect unchanging data together, but a writer needs exclusive access. A reader-preference rule can admit a continuous stream of readers and postpone a waiting writer indefinitely. Writer preference can in turn disadvantage readers. The names of the locks do not specify the fairness policy; describe admission rules and examine adverse arrival patterns.

Deadlock means the participants are waiting for events that blocked participants must cause. Starvation means one participant receives no progress while others can continue. Livelock means participants keep reacting or retrying without completing useful work. Avoiding one does not establish absence of the others. When someone says an algorithm “works,” ask which safety and liveness properties have actually been shown.

Priority inversion connects to Week 2: a high-priority thread can wait for a lock held by a low-priority thread, while medium-priority runnable work delays the owner. Priority inheritance temporarily raises an owner’s effective priority in a supported protocol so it can finish and release. It is not an automatic property of every default mutex, and it does not solve arbitrary circular waiting. Keep scheduling, resource ownership and admission policy separate in the explanation.

16. Optional practice: predict, run and explain

These activities are for practice; no grade, deadline or submission rule is introduced here. Download counter.c and queue.c. Use an Ubuntu environment with a C compiler and pthread support. Record your OS, compiler version, commands and outputs so that another student can reproduce the observation. The supplied code was compiled and exercised on the preparation machine running macOS; the Ubuntu execution path has not yet been verified.

cc -std=c11 -O2 -Wall -Wextra -pthread counter.c -o counter
./counter mutex
./counter atomic
./counter split

cc -std=c11 -O2 -Wall -Wextra -pthread queue.c -o queue
./queue
cc -std=c11 -O2 -Wall -Wextra -pthread -DCAPACITY=1 queue.c -o queue-one
./queue-one

Before running the counter, predict mutex = 200,000, atomic = 200,000 and split = 1. Read the rendezvous in split mode and explain why both loads must precede either store. The condition-variable loop itself follows the predicate discipline described earlier. The result is intentionally a failed compound increment built out of well-defined atomic accesses, so the exercise isolates the difference between atomic fields and atomic operations.

Before running the queue, mark each shared field and the mutex that protects it. Name both wait predicates and the operation that can change each one. Explain why capacity one should preserve order and total even though it forces frequent handoffs. The code checks FIFO sequence as well as aggregate count and sum, since a sum alone can hide a duplicate paired with a missing item. A timeout or hang calls for a dependency trace, not merely another run.

AI assistance is permitted. Ask an assistant to propose an adverse interleaving, then verify each step against the real lock ownership and predicate checks. Require it to state whether it is discussing an abstract model, C semantics or an observed execution. Keep the actual evidence and your explanation. Repeated runs are useful evidence, but do not replace an invariant or termination argument. These small examples do not implement crash recovery, cancellation cleanup or production queue shutdown with many producers.

Worked answer: why does the final main-thread read need no additional queue lock?

Main reads the result fields only after pthread_join has established that their worker has terminated. The result fields are written by the consumer and no worker continues modifying them. Reading before join would require a different synchronization protocol.

Worked answer: is the queue empty whenever in equals out?

Not in this representation. Both an empty queue and a full queue can have equal indices after wraparound. The protected count distinguishes these cases; replacing its predicate with only an index comparison changes the representation invariant.

Worked answer: why broadcast on closure?

Closure is a persistent state change relevant to all consumers waiting for work. Waking all of them allows each to recheck whether work remains or termination is permitted. A single signal need not wake every participant in a multiple-consumer design, though the supplied program has one consumer.

17. Review and study method

For each problem, write four lines before writing code: the shared state; its invariant; the atomic or locked transitions; and the reasons a thread may wait. Then draw an adverse interleaving. Use the bathroom key to test ownership and release, the wind-up cars to test permit accounting, the melon conveyor to test empty/full predicates, and the philosophers to test dependencies between several acquisitions. These analogies are starting points for a protocol, not substitutes for its assumptions.

1. Why does one CPU not eliminate races?

A thread can be preempted between a read and its dependent write. Another thread can update the same object during that interval even without simultaneous physical execution.

2. What does strict alternation get wrong?

It can force the only interested thread to wait for an uninterested peer. Exclusion holds in the model, but the progress requirement does not.

3. What is the difference between semaphore wait and condition wait?

A successful semaphore wait consumes a stored permit. A condition wait releases an associated mutex and waits for a possible predicate change, returning with that mutex reacquired. Its notification is not a stored permit.

4. Why is the restroom hook not a fairness guarantee?

Exclusive possession of the key prevents two owners but does not specify who gets it next. An admission rule is needed to establish bounded waiting.

5. Why cannot a condition signal replace the predicate?

A signal can occur when no one is waiting, or a waiter can awaken after the condition has become false. Persistent shared state determines whether it may proceed.

6. What does chopstick ordering prove, and what does it not prove?

A consistent increasing acquisition order prevents the modeled circular wait. It does not by itself bound waiting time, guarantee a fair scheduler, or prove that every philosopher eventually eats.

18. Textbook figures: additional examples

These figures extend the main chapter across POSIX, Java and kernel examples. Compare the shared state, admission rule, waiting predicate and release operation in each. They are optional comparison material; the practical exercises remain C/pthreads. Original source snippets are retained for discussion, with qualifications where they omit error handling or need correction. They have not been executed as a complete program.

Windows mutex states

The signaled state means the mutex is available; acquisition makes it nonsignaled. The owner releases it to make it available again.

Windows mutex states: original textbook diagram or code fragment.
Silberschatz, Galvin and Gagne, Operating System Concepts, 10th edition, Chapter 7, slide 18. Select for full size.

Interpretation and limits. Read the arrows as transitions, not a FIFO queue guarantee. This is the textbook Windows example, not a Linux mutex diagram.

Linux kernel atomic integers

Trace the arithmetic: set 5, add 10, subtract 4, increment once, then read 12, assuming no other updates. Each operation acts on the atomic object.

Linux kernel atomic integers: original textbook diagram or code fragment.
Silberschatz, Galvin and Gagne, Operating System Concepts, 10th edition, Chapter 7, slide 20. Select for full size.

Interpretation and limits. This is the kernel atomic_t interface. Our user-space C exercise uses C11 atomics. Atomicity of one counter does not protect a multi-variable invariant.

Creating a named semaphore

A name lets separate processes open the same semaphore. The initial value applies when a new semaphore is created, not every time an existing one is opened.

Creating a named semaphore: original textbook diagram or code fragment.
Silberschatz, Galvin and Gagne, Operating System Concepts, 10th edition, Chapter 7, slide 24. Select for full size.

Interpretation and limits. For portable practice use a leading-slash name such as /cse513_demo. Check SEM_FAILED; choose permissions deliberately and plan sem_close/sem_unlink cleanup.

Using a named semaphore

A successful sem_wait consumes a permit; sem_post supplies one. Here a single permit guards the critical section.

Using a named semaphore: original textbook diagram or code fragment.
Silberschatz, Galvin and Gagne, Operating System Concepts, 10th edition, Chapter 7, slide 24. Select for full size.

Interpretation and limits. This screenshot omits error handling. Handle an interrupted or failed wait before entering, and do not post as though a failed acquisition succeeded.

Initializing a condition variable

The mutex protects the shared predicate. The condition variable lets threads wait for changes that may make that predicate true.

Initializing a condition variable: original textbook diagram or code fragment.
Silberschatz, Galvin and Gagne, Operating System Concepts, 10th edition, Chapter 7, slide 26. Select for full size.

Interpretation and limits. Initialize both before concurrent use; check return codes. Destroy them only after all users and waiters have finished. A condition variable does not store permits.

Java bounded-buffer state

The buffer holds at most five items. count tracks occupancy; in and out wrap around the array. Synchronized methods use the same object monitor.

Java bounded-buffer state: original textbook diagram or code fragment.
Silberschatz, Galvin and Gagne, Operating System Concepts, 10th edition, Chapter 7, slide 30. Select for full size.

Interpretation and limits. Connect this state to the melon conveyor: count == 0 means empty and count == BUFFER_SIZE means full. The next producer/consumer figures provide the method bodies.

Java monitor entry set

A thread seeking an occupied object monitor cannot enter its synchronized region. The entry set represents contenders for that monitor.

Java monitor entry set: original textbook diagram or code fragment.
Silberschatz, Galvin and Gagne, Operating System Concepts, 10th edition, Chapter 7, slide 31. Select for full size.

Interpretation and limits. Compare with the wait-set figure in the main lecture: waiting for the lock and waiting for a predicate are different. The drawing does not promise FIFO admission.

Java bounded-buffer producer

While the buffer is full, wait releases the monitor. On return, the thread reacquires it and checks count again before inserting and advancing in.

Java bounded-buffer producer: original textbook diagram or code fragment.
Silberschatz, Galvin and Gagne, Operating System Concepts, 10th edition, Chapter 7, slide 34. Select for full size.

Interpretation and limits. The source uses notify and ignores interruption. For a general multi-producer/multi-consumer version with one wait set, use notifyAll and an explicit interruption policy.

Java bounded-buffer consumer

While empty, wait releases the monitor. After reacquisition and rechecking, remove the item, advance out and decrement count.

Java bounded-buffer consumer: original textbook diagram or code fragment.
Silberschatz, Galvin and Gagne, Operating System Concepts, 10th edition, Chapter 7, slide 35. Select for full size.

Interpretation and limits. Compare each transition with our C queue. The source has no close/drain protocol and discards interruption; it is a teaching fragment, not our runnable practice program.

Java explicit lock and cleanup

lock obtains the ReentrantLock. The try/finally structure releases it when the protected body completes or throws an exception.

Java explicit lock and cleanup: original textbook diagram or code fragment.
Silberschatz, Galvin and Gagne, Operating System Concepts, 10th edition, Chapter 7, slide 36. Select for full size.

Interpretation and limits. Place acquisition before the try whose finally unlocks. Release only a lock actually acquired, and keep every access to the invariant under the same discipline.

Java semaphore permits

The image sketches the constructor parameter: its value establishes the initial permit count. One permit can model exclusive admission; more permit bounded concurrency.

Java semaphore permits: original textbook diagram or code fragment.
Silberschatz, Galvin and Gagne, Operating System Concepts, 10th edition, Chapter 7, slide 37. Select for full size.

Interpretation and limits. This line is an API sketch, not a complete declaration to paste into a class. The ordinary one-argument Java Semaphore constructor does not guarantee FIFO ordering.

Java semaphore: find the permit bug

Inspect this original snippet critically: acquire can throw InterruptedException before obtaining a permit, but the finally block still releases one.

Java semaphore: find the permit bug: original textbook diagram or code fragment.
Silberschatz, Galvin and Gagne, Operating System Concepts, 10th edition, Chapter 7, slide 37. Select for full size.

Interpretation and limits. That path creates an extra permit. Fix by acquiring before entering the try/finally release region, or track successful acquisition. Never silently discard the interruption policy.

Java lock-associated condition

newCondition creates a condition associated with this ReentrantLock. await releases that lock while waiting and reacquires it before returning normally.

Java lock-associated condition: original textbook diagram or code fragment.
Silberschatz, Galvin and Gagne, Operating System Concepts, 10th edition, Chapter 7, slide 38. Select for full size.

Interpretation and limits. Hold the lock when checking the predicate, waiting or signaling. Wait in a while loop because notification alone does not establish the predicate.

One condition per turn

Five conditions share one lock. A separate turn variable identifies which thread may work; each condition provides a waiting location.

One condition per turn: original textbook diagram or code fragment.
Silberschatz, Galvin and Gagne, Operating System Concepts, 10th edition, Chapter 7, slide 39. Select for full size.

Interpretation and limits. The condition objects do not store whose turn it is. Initialize turn and protect it with the lock. A stalled designated thread can stop this strict-turn protocol.

Turn-taking: recheck after waking

The figure tests threadNumber != turn with if before await. General Java condition code must use while so a spurious wakeup cannot authorize the wrong thread.

Turn-taking: recheck after waking: original textbook diagram or code fragment.
Silberschatz, Galvin and Gagne, Operating System Concepts, 10th edition, Chapter 7, slide 40. Select for full size.

Interpretation and limits. The empty catch also hides interruption. Define cancellation behavior and keep turn updates/signaling under the lock; the next turn must correspond to a participating thread.

An update protected by a lock

The acquire/update/release sequence makes cooperating callers execute the protected update one at a time. Identify exactly which shared state belongs to the invariant.

An update protected by a lock: original textbook diagram or code fragment.
Silberschatz, Galvin and Gagne, Operating System Concepts, 10th edition, Chapter 7, slide 42. Select for full size.

Interpretation and limits. All conflicting access must follow the same lock. Real code needs a defined cleanup path so an error or exception cannot leave the lock held.

Transactional-memory notation

The atomic block expresses a desired transaction over the enclosed reads and writes: commit as a unit, or handle a conflict according to the supporting system.

Transactional-memory notation: original textbook diagram or code fragment.
Silberschatz, Galvin and Gagne, Operating System Concepts, 10th edition, Chapter 7, slide 42. Select for full size.

Interpretation and limits. This is conceptual textbook notation, not standard C or Java syntax. Do not confuse a multi-operation transaction with a single atomic integer instruction.

Interface references: linux, sem, java, condition, object.

19. References and further study

The lecture is adapted from Abraham Silberschatz, Peter B. Galvin and Greg Gagne, Operating System Concepts, 10th edition, Chapter 6 (Synchronization Tools) and Chapter 7 (Synchronization Examples). Retained figures are identified in their captions; new prose, worked traces, programs and classroom-analogy illustrations are prepared for CSE513. The original official chapter slides provide the broader reference context.

For complementary explanations, use the freely available OSTEP chapters on concurrency, locks, condition variables and semaphores. They are linked rather than redistributed. API and language references are linked at the relevant explanations above. No additional required viewing is assigned in this chapter.

Search CSE 513