The Big Picture: One Pen, Two Writers
Two people share a notebook. Both want to write the next line on page 5 at the same moment. If both write at once, you get a mess. A simple rule fixes it: only the person holding the pen may write. Pass the pen when done.
Synchronization means making threads coordinate, so they use shared things in an orderly way. The "pen" in our story is a lock.
Without a rule With a lock ("the pen")
Thread A ──► writes ┐ Thread A ── takes lock ── writes ── gives lock
Thread B ──► writes ┴─► MESS Thread B ───── waits ─────────────── takes lock ── writes
7.1 Why Synchronization Is Needed (Shared Data)
What it is: Threads of one process share the heap and global variables (Section 1). That is handy, but if two threads change the same data at once, the result can be wrong.
The classic example: a counter. The line count = count + 1 looks like one step. To the CPU it is three:
1. LOAD count from memory into a register
2. ADD 1 to the register
3. STORE the register back to memory
Now imagine two threads both do this when count is 10:
time ─► Thread A Thread B memory: count
1 LOAD (reg = 10) 10
2 LOAD (reg = 10) 10
3 ADD (reg = 11) 10
4 ADD (reg = 11) 10
5 STORE (count = 11) 11
6 STORE (count = 11) 11 ← should be 12!
Two increments happened, but the counter only went up by one. One update was lost. This is a race condition (Section 8.1).
Key terms:
| Term | Plain meaning |
|---|---|
| Shared data | Anything more than one thread can read or write (heap, globals) |
| Critical section (the idea) | A stretch of code that touches shared data and must run by one thread at a time |
| Mutual exclusion | The rule: only one thread inside the critical section at once |
| Atomic | An action that cannot be cut in half; other threads see it either not done or fully done |
Not everything needs a lock. Data that is only read by all threads, or data that each thread owns (like local variables on its own stack), is safe. Locks are needed only when at least one thread writes shared data.
A cheap option: atomic operations. The CPU has special instructions that do a whole read-modify-write in one uninterruptible step. Windows exposes them as the Interlocked functions:
InterlockedIncrement(&count); // safe add 1, no lock needed
InterlockedCompareExchange(&x, new, expected); // set x to new only if x == expectedThese are very fast. They suit simple jobs (counters, flags, linked-list pointers). For bigger jobs (changing several things together) you need a real lock.
Cost of locking: locks make threads wait. Too much locking turns a multithreaded program into a single-file queue. The goal is to lock as little, and as briefly, as you can.
7.2 Mutex (Mutual Exclusion)
What it is: A mutex is a lock with one owner at a time. A thread acquires (locks) it before using shared data and releases (unlocks) it afterwards. If another thread tries to acquire it while it is held, that thread waits (state Waiting, Section 4).
Windows mutex facts:
| Fact | Detail |
|---|---|
| Created with | CreateMutex (a kernel object; you get a handle) |
| Acquire / release | WaitForSingleObject(h, timeout) / ReleaseMutex(h) |
| Has an owner | Only the owner may release it |
| Recursive | The same thread can acquire it again; it must release the same number of times |
| Cross-process | A mutex can have a name, so threads in different processes can share it |
| Abandoned | If the owner thread ends without releasing, the next waiter is told it was "abandoned" so it can check for damage |
| Speed | Slower than a critical section, because every acquire may enter the kernel |
Safe pattern:
acquire(mutex)
... touch shared data ...
release(mutex) ← always, even if something goes wrong
Real-life use: A famous trick uses a named mutex so a program can start only once. The second copy tries to create the same named mutex, finds it already exists, and quits.
7.3 Semaphore (Counting Resource Access)
What it is: A semaphore is like a mutex, but it holds a count. It lets up to N threads in at once.
Think of a car park with 5 spaces. A sign shows how many are free. Each car entering takes a space (count goes down). Each car leaving frees one (count goes up). At 0, the next car must wait at the gate.
Semaphore count = 3 (3 free "slots")
Thread 1 enters ► count 2
Thread 2 enters ► count 1
Thread 3 enters ► count 0
Thread 4 arrives ► must WAIT
Thread 2 leaves ► count 1 ► Thread 4 may enter ► count 0
Windows semaphore facts:
| Fact | Detail |
|---|---|
| Created with | CreateSemaphore(initialCount, maximumCount, name) |
| Take a slot | WaitForSingleObject (count goes down by 1; waits at 0) |
| Give a slot back | ReleaseSemaphore (count goes up) |
| No owner | Any thread may release. It is not tied to one thread |
| Can be named | Shared across processes |
Uses:
| Use | Example |
|---|---|
| Limit access | Allow at most 4 threads to talk to the database at once |
| Producer/consumer | A "queue has items" semaphore wakes consumers when producers add work |
| A binary semaphore (max 1) | Looks like a mutex, but with no owner |
7.4 Event (Signaling Between Threads)
What it is: An event is a simple flag that threads can wait on. It is for telling threads "something has happened", not for guarding data.
Think of a traffic light: cars (threads) wait at red (non-signalled) and go when it turns green (signalled).
| Kind | Behaviour |
|---|---|
| Manual-reset event | Once set, it stays signalled until someone resets it. All waiting threads are released. Good for "start now" or "shut down" |
| Auto-reset event | After it releases one waiting thread, it resets itself. Good for passing one job at a time |
Windows functions: CreateEvent, SetEvent (turn green), ResetEvent (turn red), WaitForSingleObject.
Worker thread: Main thread:
WaitForSingleObject(dataReady) ... prepare data ...
(state: Waiting, uses no CPU) SetEvent(dataReady) ──► worker wakes up
... process the data ...
A waiting thread uses no CPU. This is far better than spinning (a loop that keeps asking "ready yet?" and wastes a core).
Common uses: "data is ready", "a file finished loading", "please stop" (the stop-signal from Section 2.4), or "the program is initialised".
Wait on several at once: WaitForMultipleObjects lets a thread wait for any (or all) of a list of events, mutexes and so on.
7.5 Critical Section (Lightweight Lock)
What it is: In Windows, a CRITICAL_SECTION is a fast mutex that works inside one process only. Despite the confusing name, it is a lock object (do not mix it with the idea of "a critical section of code").
Why it is faster than a mutex:
Mutex: every acquire = call into the kernel = slow
Critical section: free lock? grab it with one atomic instruction in user mode = fast
lock held? spin briefly, then ask the kernel to put the thread to sleep
Windows functions: InitializeCriticalSection, EnterCriticalSection, LeaveCriticalSection, DeleteCriticalSection (and TryEnterCriticalSection).
Comparison:
| Feature | Critical Section | Mutex |
|---|---|---|
| Scope | One process only | Can be shared between processes (if named) |
| Speed | Faster when uncontended | Slower (always a kernel call) |
| Kernel object (handle)? | No | Yes |
| Recursive? | Yes | Yes |
| Wait with timeout? | Only TryEnter | Yes |
| Abandoned detection? | No | Yes |
Slim Reader/Writer lock (SRW lock). Windows also has SRWLOCK, an even smaller and faster lock (the size of one pointer). It has two modes:
| Mode | Meaning |
|---|---|
| Shared (readers) | Many threads may read at the same time |
| Exclusive (writer) | Only one thread may write, and no readers while it does |
Good for data that is read often and changed seldom. Functions: AcquireSRWLockShared, AcquireSRWLockExclusive, and the matching Release calls. SRW locks are not recursive.
Which should I choose?
| Situation | Choose |
|---|---|
| Just counting or flagging | Interlocked (atomic) |
| Many readers, few writers, one process | SRW lock |
| Ordinary shared data, one process | Critical section (or SRW exclusive) |
| Threads in different processes | Named mutex |
| Limit to N users | Semaphore |
| "Tell me when X happens" | Event |
7.6 Condition Variables
What it is: A condition variable lets a thread sleep until a condition becomes true, and releases its lock while sleeping. It is always used together with a lock.
Think of waiting at a restaurant: "Give me your phone number and sit anywhere. We'll call when a table is free." You are not blocking the door while you wait.
The pattern (a producer/consumer queue):
CONSUMER thread PRODUCER thread
lock lock
while (queue is empty) add item to queue
wait(condition, lock) ◄─ sleeps, unlock
(lock is released) │ signal(condition) ──► wakes a waiter
take item │
unlock woken: lock re-acquired
Why while and not if? A woken thread must re-check the condition. Another thread may have taken the item first, and a thread can occasionally be woken for no real reason ("spurious wakeup"). Re-checking is a safe habit.
Windows functions: CONDITION_VARIABLE with InitializeConditionVariable, SleepConditionVariableCS (with a critical section), SleepConditionVariableSRW (with an SRW lock), WakeConditionVariable (wake one) and WakeAllConditionVariable (wake all).
Event or condition variable? An event is its own signal (a kernel object, can cross processes). A condition variable is lighter and fits "wait until this shared data looks right" inside one process.
7.7 Quick Rules for Safe Locking
| Rule | Reason |
|---|---|
| Hold locks for the shortest time | Others wait while you hold it |
| Never do slow things (disk, network) while holding a lock | Everything piles up |
| Always release, even on errors | Otherwise others wait forever |
| Lock in the same order everywhere | Prevents deadlock (Section 8.2) |
| Prefer less shared data | No sharing means no locks |
| Do not guess; use tools | Thread analysers and testing find bugs |
Summary
| Tool | One-line meaning | Cross-process? |
|---|---|---|
| Interlocked (atomic) | One uninterruptible read-modify-write | Memory only |
| Mutex | One owner at a time; owner must release | Yes (named) |
| Semaphore | Count of free slots; N at a time; no owner | Yes (named) |
| Event | A flag threads wait on; manual or auto reset | Yes (named) |
| Critical section | Fast mutex for one process | No |
| SRW lock | Tiny lock; many readers or one writer | No |
| Condition variable | Sleep until a condition is true, releasing a lock while asleep | No |