Process Synchronization & the Critical-Section Problem

a semaphore

/ SEM-uh-for /

A semaphore, invented by Dijkstra, is a synchronization tool built around a single integer counter plus two atomic operations. The classic picture is a tray of restaurant pagers: the count is how many pagers are left on the tray. When a party arrives they take one (the count drops); if the tray is empty they must wait until someone returns one. When a party finishes they put a pager back (the count rises), which lets a waiting party take it. The semaphore manages exactly this 'how many of a thing are available, and who waits when there are none' bookkeeping.

It offers two operations, historically named P and V (Dutch, from Dijkstra), and today usually called wait and signal. wait(S) decrements the count; if the count would go negative, the calling thread blocks (sleeps) until the count is positive again. signal(S) increments the count; if any threads are waiting, one of them is woken to proceed. Both operations are performed atomically, so the count can never be corrupted by two threads at once. Critically, a well-built semaphore does not busy-wait: wait puts a blocked thread onto a waiting queue and gives up the CPU, and signal moves one thread off that queue — no spinning, the count and the queue are managed by the OS.

A semaphore does two jobs that look different but are the same mechanism. As a counting semaphore initialized to N, it lets up to N threads through at once — perfect for managing N identical resources (N database connections, N free buffer slots). Initialized to 1, it acts as a lock providing mutual exclusion (a binary semaphore). And used asymmetrically — one thread only ever waits, another only ever signals — it becomes a way to signal events between threads, e.g. 'the data is ready now'. The crucial honesty: a semaphore protects nothing by itself. It only works if every thread touching the shared data calls wait and signal correctly and in the right order; one missing wait, one signal without a matching wait, or a wait/signal swapped, and you get a race, a deadlock, or a lost wakeup. Its power comes entirely from disciplined, consistent use.

A pool of 3 printers: init a counting semaphore S = 3. Each job does wait(S) (taking a printer; the 4th job blocks until one is free), prints, then signal(S) (returning it, waking a waiter). Set S = 1 instead and the same code becomes a plain mutex around a single resource.

wait(S) takes one (may block); signal(S) returns one (may wake a waiter). Count = how many free.

A semaphore enforces nothing on its own — it works only if every thread uses wait/signal correctly. One careless access, a missing wait, or a swapped wait/signal silently breaks it.

Also called
counting semaphore信號量旗號