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:

TermPlain meaning
Shared dataAnything 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 exclusionThe rule: only one thread inside the critical section at once
AtomicAn 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 == expected

These 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:

FactDetail
Created withCreateMutex (a kernel object; you get a handle)
Acquire / releaseWaitForSingleObject(h, timeout) / ReleaseMutex(h)
Has an ownerOnly the owner may release it
RecursiveThe same thread can acquire it again; it must release the same number of times
Cross-processA mutex can have a name, so threads in different processes can share it
AbandonedIf the owner thread ends without releasing, the next waiter is told it was "abandoned" so it can check for damage
SpeedSlower 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:

FactDetail
Created withCreateSemaphore(initialCount, maximumCount, name)
Take a slotWaitForSingleObject (count goes down by 1; waits at 0)
Give a slot backReleaseSemaphore (count goes up)
No ownerAny thread may release. It is not tied to one thread
Can be namedShared across processes

Uses:

UseExample
Limit accessAllow at most 4 threads to talk to the database at once
Producer/consumerA "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).

KindBehaviour
Manual-reset eventOnce set, it stays signalled until someone resets it. All waiting threads are released. Good for "start now" or "shut down"
Auto-reset eventAfter 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:

FeatureCritical SectionMutex
ScopeOne process onlyCan be shared between processes (if named)
SpeedFaster when uncontendedSlower (always a kernel call)
Kernel object (handle)?NoYes
Recursive?YesYes
Wait with timeout?Only TryEnterYes
Abandoned detection?NoYes

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:

ModeMeaning
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?

SituationChoose
Just counting or flaggingInterlocked (atomic)
Many readers, few writers, one processSRW lock
Ordinary shared data, one processCritical section (or SRW exclusive)
Threads in different processesNamed mutex
Limit to N usersSemaphore
"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

RuleReason
Hold locks for the shortest timeOthers wait while you hold it
Never do slow things (disk, network) while holding a lockEverything piles up
Always release, even on errorsOtherwise others wait forever
Lock in the same order everywherePrevents deadlock (Section 8.2)
Prefer less shared dataNo sharing means no locks
Do not guess; use toolsThread analysers and testing find bugs

Summary

ToolOne-line meaningCross-process?
Interlocked (atomic)One uninterruptible read-modify-writeMemory only
MutexOne owner at a time; owner must releaseYes (named)
SemaphoreCount of free slots; N at a time; no ownerYes (named)
EventA flag threads wait on; manual or auto resetYes (named)
Critical sectionFast mutex for one processNo
SRW lockTiny lock; many readers or one writerNo
Condition variableSleep until a condition is true, releasing a lock while asleepNo