Operating systems and concurrency
Mutexes, semaphores and condition variables
The three basic blocking primitives. A mutex gives exclusive access, a semaphore counts available resources, and a condition variable waits for a state change.
Three primitives cover most blocking synchronization. A mutex lets one thread into a section at a time. A semaphore counts a pool of identical resources. A condition variable lets a thread sleep until some state becomes true. Each one answers a different question, and mixing them up causes most misuse.
Walking through it: the mutex
A mutex (mutual exclusion lock) has two operations, lock and unlock. Only one thread holds it at a time. Any other thread that calls lock blocks until the holder unlocks. Use it to protect a critical section, the code that reads and writes shared state together.
The race from the race conditions page disappears with a lock:
import threading
counter = 0
lock = threading.Lock()
def work():
global counter
for _ in range(100_000):
with lock: # only one thread in this block at a time
counter += 1
ts = [threading.Thread(target=work) for _ in range(4)]
for t in ts: t.start()
for t in ts: t.join()
print("with lock:", counter)Output:
with lock: 400000The with block releases the lock even when an exception is raised. Prefer that form to manual lock/unlock pairs, which leak the lock on an error path.
Semaphores
A semaphore holds a count. acquire decrements it and blocks at zero. release increments it. A counting semaphore limits concurrency to N, such as 10 open database connections or 5 parallel downloads. A binary semaphore with a count of 1 looks like a mutex, but it has no owner. Any thread can release it, which makes it easy to misuse.
Use a mutex when the same thread must release what it took, and a semaphore when you're rationing a pool.
Condition variables
A condition variable is for waiting until something is true, such as "the queue is not empty". The waiting thread releases the lock and sleeps, and wakes up when another thread signals. Because wakeups can be spurious, always check the condition in a loop:
lock.acquire()
while not queue: # loop, not if: wakeups can be spurious
cond.wait() # releases lock while sleeping, reacquires on wake
item = queue.pop(0)
lock.release()The producer does the same under the lock, then calls notify. The pattern is a mutex with a waiting room. The condition variable is never a substitute for a lock, since it only works together with one.
Note
Most languages ship a higher-level queue, such as Python's queue.Queue or Go channels. Use them before writing a condition-variable queue by hand.
Common mistakes
- Using
ifinstead ofwhilearoundwait, so a spurious wakeup proceeds with a false condition. - Calling
waitwithout holding the lock. - Using a binary semaphore where a mutex's ownership rule is what you need.
- Locking across a slow call, which makes every other thread wait for the network.
Check yourself
- Why is a condition variable always paired with a lock? Answer: checking the condition and sleeping must be atomic, or a signal can arrive between the check and the sleep and be lost.
- Why limit database connections with a counting semaphore instead of a mutex? Answer: a mutex allows one at a time. You want N in parallel, which is a counting semaphore.
Key takeaways
- A mutex gives exclusive access, and the holder must unlock it.
- A counting semaphore limits how many can use a pool at once.
- A condition variable waits for a state change, always inside a
whileloop. - Use the language's
withform and higher-level queues where you can.
Further learning
Next, see atomics and memory ordering for synchronization that doesn't block at all.
Related
Concurrency
Why shared state breaks when two threads touch it at once, and the tools that fix it, from locks and semaphores to atomics, message passing and database constraints.
Race conditions
A race condition happens when the result depends on the order in which threads run. How a read-modify-write loses an update, and why check-then-act bugs are so common.
Deadlock and livelock
Deadlock is threads waiting on each other forever. The four Coffman conditions that must all hold, the fixes that break one of them, and how livelock differs.