1. Beyond Mutual Exclusion
Mutexes are great for protecting critical sections, but they are limited. What if we want to coordinate threads? For example:
- “Thread A must finish task X before Thread B starts task Y.”
- “Allow at most 5 threads to access the database.”
Enter the Semaphore, invented by Edsger W. Dijkstra.
The Definition
A semaphore is an integer variable that, apart from initialization, is accessed only through two standard atomic operations: P and V.
- P(s) (Proberen / Test / Wait):
- If
s > 0, decrementsand continue. - If
s == 0, wait (block) untils > 0.
- If
- V(s) (Verhogen / Increment / Signal):
- Increment
s. - If any threads were blocked on
P, wake one up.
- Increment
Binary vs Counting
- Binary Semaphore (initialized to 1): Acts exactly like a Mutex.
- Counting Semaphore (initialized to N): Used to control access to a finite resource pool (e.g., Connection Pool).
2. The Producer-Consumer Problem
This is the classic concurrency problem. We have a fixed-size buffer.
- Producer: Generates data and puts it into the buffer. Must wait if buffer is Full.
- Consumer: Takes data out of the buffer. Must wait if buffer is Empty.
Solution with Semaphores
We need three semaphores:
empty: Initialized to N (Size of buffer). tracks empty slots.full: Initialized to 0. Tracks filled slots.mutex: Initialized to 1. Protects buffer access.
3. Interactive: Ring Buffer Visualizer
Visualize how a Producer and Consumer coordinate over a fixed-size buffer (Size = 5). Notice how the Producer blocks when the buffer is full, and the Consumer blocks when it is empty.
4. Code Example: Producer-Consumer
In Go, we don’t usually use raw semaphores. We use Buffered Channels, which act exactly like a semaphore-protected buffer.