Synchronization Primitives
Mutexes and the scope you put them around, read/write locks, semaphores as permit counters, condition variables and the predicate loop — with lost and spurious wakeups as the failures that follow from getting the loop wrong.
A mutex gives you one thing: at most one task inside the region at a time. Operating Systems covers how that is implemented. What matters here is which invariant a given mutex is protecting, what it costs the tasks that wait, and the long list of things engineers expect from it that it does not provide.
Q · Which invariant does this particular mutex protect, and which of the guarantees I am assuming does it actually give me?
The single highest-yield concurrency review question: what is inside this lock that does not need to be? A lock held across a network call has a hold time set by someone else's p99, which makes their outage your outage and their latency your throughput ceiling.
Q · What is inside this critical section that does not belong there, and what does keeping it there cost?
Many readers or one writer. The idea is obviously good and the practice frequently is not: a read/write lock has a more expensive uncontended path than a mutex, it can starve writers, and it only pays when reads dominate *and* the region is long enough for the concurrency to matter. Most of the time a plain mutex or an immutable snapshot wins.
Q · Does allowing readers to proceed in parallel actually pay here, or am I adding overhead and a starvation risk for nothing?
A semaphore is a counter with a waiting room. Its natural use is not mutual exclusion but *resource limiting*: at most N tasks may be doing this at once, where N is the size of something real — a connection pool, an upload buffer, a downstream partner's rate limit. The failure that matters is the leaked permit.
Q · How do I express "at most N of these at a time", and what happens to the permit when the task in the middle throws?
A binary semaphore and a mutex both admit one task at a time, which is where the similarity ends. A mutex has an owner; a semaphore has a count. That single difference decides reentrancy, priority inheritance, who is allowed to release, whether the primitive can signal across tasks, and which failures are possible at all.
Q · When the two primitives look identical at N = 1, what actually differs, and which failures does each one make possible?
A mutex answers "may I touch this?". A condition variable answers "is it worth touching yet?". It lets a task release the lock and sleep until another task says the state changed — and every correct use of one is built around a predicate checked in a loop, under the lock, both before waiting and after waking.
Q · How does a task wait for a *condition* on shared state without holding the lock and without spinning?
A condition variable stores nothing. If the state changes and the notification is sent at a moment when nobody is waiting, that notification is discarded — and the task that arrives a microsecond later waits forever for an event that already happened. The fix is not a bigger buffer or a retry; it is checking the predicate under the same lock that guards the state change.
Q · Why does a task wait forever for a condition that is already true, and what exactly must be under the lock to prevent it?
A condition variable may return from wait without any notification having been sent. That is permitted behaviour in POSIX, in C++, in Java and in Python — and even where it is not, another task can consume the state between the notify and your wake. Both reasons lead to the same rule: re-check the predicate in a loop, always.
Q · Why must the predicate be re-checked after `wait` returns, and which languages and APIs does that apply to?