Module 2 · Unit 6 of 5
Synchronization
The hardest unit in the course — and the most important. Concurrent programs that share mutable state must be synchronised, or they will produce results that depend on timing. This unit develops the theory of critical sections and the toolbox of synchronization primitives that make correct concurrent code possible.
Module 2 · Unit 6
Synchronization
Seven lessons on the theory and practice of coordinating concurrent processes and threads. The material is dense; the labs make it concrete.
01 Unit learning outcomes
Upon completion of this unit you will be able to
- U6.1 — Explain concurrency problems.
- U6.2 — Identify race conditions.
- U6.3 — Apply synchronization mechanisms.
- U6.4 — Design synchronization solutions.
- U6.5 — Implement Linux synchronization solutions.
This unit addresses course learning outcomes CLO3, CLO6, and CLO8. It is the primary source material for Assignment 1.
02 Lessons in this unit
The first two lessons define the problem. The next five develop the mechanisms, from the simplest (locks) to the most structured (monitors and condition variables), and close with the Linux facilities you will use in practice.
Race Conditions
What a race is, why non-determinism is so damaging, and how to recognise the pattern.
Lesson 6.2Critical Sections
The three properties a correct solution must satisfy, and the classical attempts to achieve them.
Lesson 6.3Locks
Spinlocks, blocking locks, and the hardware primitives (test-and-set, compare-and-swap) they rely on.
Lesson 6.4Semaphores
Counting and binary semaphores, and the classic producer–consumer and readers–writers patterns.
Lesson 6.5Monitors
The structured synchronization abstraction that bundles a lock and condition variables.
Lesson 6.6Condition Variables
Waiting for a predicate to become true — and why the wait must always be in a while loop.
Lesson 6.7Synchronization in Linux
Mutexes, semaphores, futexes, atomic operations, and the C11 memory model.
03 What you will learn, lesson by lesson
6.1 Race Conditions
- Define a race condition.
- Identify interleaving hazards.
- Explain non-determinism.
- Analyse outcomes.
- Recognise race patterns.
6.2 Critical Sections
- Define the critical-section problem.
- State mutual exclusion.
- State progress.
- State bounded waiting.
- Evaluate proposed solutions.
6.3 Locks
- Define a lock.
- Compare spin and blocking locks.
- Explain test-and-set.
- Explain compare-and-swap.
- Analyse lock performance.
6.4 Semaphores
- Define semaphores.
- Use counting semaphores.
- Use binary semaphores.
- Solve producer–consumer.
- Analyse semaphore pitfalls.
6.5 Monitors
- Define a monitor.
- Explain monitor semantics.
- Compare with locks.
- Analyse monitor design.
- Evaluate monitor use.
6.6 Condition Variables
- Define condition variables.
- Explain wait and signal.
- Explain the while-loop rule.
- Analyse lost wakeups.
- Design predicate-based waits.
6.7 Synchronization in Linux
- Use pthread mutexes.
- Use POSIX semaphores.
- Explain futexes.
- Apply atomic operations.
- Analyse memory-ordering issues.
04 Worked example
The producer–consumer problem, solved with a mutex and two condition variables. This pattern appears in every message queue and every thread pool.
#include <pthread.h>
#define N 16
int buffer[N];
int count = 0;
int in = 0, out = 0;
pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t not_full = PTHREAD_COND_INITIALIZER;
pthread_cond_t not_empty = PTHREAD_COND_INITIALIZER;
void *producer(void *arg) {
for (int i = 0; i < 100; i++) {
pthread_mutex_lock(&m);
while (count == N)
pthread_cond_wait(¬_full, &m);
buffer[in] = i;
in = (in + 1) % N;
count++;
pthread_cond_signal(¬_empty);
pthread_mutex_unlock(&m);
}
return NULL;
}
void *consumer(void *arg) {
for (int i = 0; i < 100; i++) {
pthread_mutex_lock(&m);
while (count == 0)
pthread_cond_wait(¬_empty, &m);
int v = buffer[out];
out = (out + 1) % N;
count--;
pthread_cond_signal(¬_full);
pthread_mutex_unlock(&m);
}
return NULL;
}
Why every line is there
-
The mutex
mguards all shared state.count,in,out, and the buffer contents can only be touched while holding the lock. -
The condition variables signal state changes.
not_fullis signalled when the producer adds an item;not_emptyis signalled when the consumer removes one. -
The wait is inside a
whileloop. This is the single most important rule in this lesson. Whenpthread_cond_waitreturns, the thread does not necessarily own the predicate it was waiting for. Another thread may have consumed the condition before this one reacquired the mutex. The loop re-checks. - The signal is issued while holding the lock. This is not strictly required by POSIX, but it is the clearest discipline: signal after you have changed the predicate, before you release the lock. It eliminates a class of subtle races that are extremely difficult to debug.
- Both threads return the mutex before re-looping. Holding the lock across an unbounded operation would block the other side and defeat the purpose of the pattern.
Use while, not if
Using if (count == N) instead of
while (count == N) is the single most common
synchronization bug. It causes failures that reproduce once in
a million runs and are almost impossible to reproduce on
demand.
05 Required reading
- OSTEP — Chapters 28–34 (locks, condition variables, semaphores, and the classic concurrency problems).
- Operating System Concepts — Chapter 6, Synchronization Tools, and Chapter 7, Synchronization Examples.
-
Man pages:
pthread_mutex_lock(3),pthread_cond_wait(3),sem_overview(7),futex(2).
Read slowly
The material in this unit is dense and the details matter. Multiple readings of the same chapter with code in hand is the normal approach. Do not try to skim.
06 Unit discussion
Discussion prompt
Some modern languages and libraries avoid shared-memory synchronization altogether by having concurrent tasks communicate only through message channels — Go's "Do not communicate by sharing memory; instead, share memory by communicating" is the canonical slogan. Others, such as Rust, use the type system to make data races a compile-time error.
Pick one of these approaches and explain how it reduces the class of bugs this unit has been describing. Then identify a class of bug it does not eliminate. Support your answer with a concrete example.
Post your response to the Unit 6 discussion forum and reply to at least two classmates. This activity contributes directly to Assignment 1's synchronization design criterion.
07 Laboratory connection
Lab 5 — Synchronization is completed at the end of this unit. It asks you to implement the classical concurrency problems from scratch, deliberately break them to observe failures, and then repair them.
| Lesson | Lab 5 exercise |
|---|---|
| 6.1 Race Conditions | Reproduce a race with two threads incrementing a counter |
| 6.3 Locks | Fix the race with a pthread mutex; measure the cost |
| 6.4 Semaphores | Implement producer–consumer with POSIX semaphores |
| 6.6 Condition Variables | Implement the same with mutex + condition variables |
| 6.7 Linux | Inspect blocking with strace -f and ltrace |
08 Knowledge check & quiz
Quick check before you begin
Question 1
State the three properties a correct critical-section solution must satisfy.
Show answer
Question 2
Why must pthread_cond_wait always be called inside
a while loop that re-checks the predicate, rather
than an if?
Show answer
pthread_cond_wait can wake up spuriously,
and even a signal-driven wakeup does not guarantee that the
predicate is still true. Between the signal and the moment
the woken thread reacquires the mutex, another thread may
have consumed the condition the signal announced. The
while loop re-evaluates the predicate under the
protection of the mutex, so the thread either proceeds
safely or goes back to waiting.
Question 3
What is a futex, and why is it important for the performance of Linux synchronization primitives?