AlgoMaster Logo

Mutexes, Semaphores, and Condition Variables

22 min readUpdated August 7, 2026
Listen to this chapter
Unlock Audio

A request handler needs exclusive access to a shared in-memory index. A connection pool allows at most ten simultaneous borrowers. A worker must sleep until configuration data becomes ready.

All three situations require synchronization, but they are not the same problem:

GoalPrimitive
Protect one critical sectionA mutex
Represent a limited number of usesA semaphore
Wait until shared state changesA condition variable

Using the right primitive makes the program's correctness argument visible in the code. Using the wrong one often creates missing wakeups, leaked capacity, or state that is technically protected but still logically inconsistent.

Mutex Protection of Critical Sections

A mutex, short for mutual exclusion, allows one thread at a time to own the mutex.

A thread locks the mutex before entering a protected critical section and unlocks it afterward:

Lock the mutex, read and modify the protected state, then unlock the mutex.

If another thread tries to lock the same mutex while it is owned, that thread must wait until the owner unlocks it.

The mutex is not attached to a variable by the hardware or C type system. The association is a program rule:

Every access that can conflict must use the same mutex.

If writers lock the mutex but readers access the object without it, the object is not protected. If two functions use different mutexes for the same state, they can still enter their critical sections simultaneously.

Invariant Protection Beyond Individual Variables

Suppose a service tracks jobs in two states:

Moving one job must decrement pending and increment running while preserving the total. Both fields belong to one invariant, so one mutex protects the complete transition:

The critical section includes the check and both updates. Protecting only the assignments would leave a check-then-act race.

This example keeps error handling compact, but production code must also handle a failed pthread_mutex_unlock. POSIX thread functions return an error number directly; they do not generally report these failures through errno.

The fields can remain ordinary integers as long as every conflicting access follows the mutex protocol. Locking and unlocking provide the visibility and ordering needed for the protected data. Adding volatile is neither necessary nor sufficient.

Importance of Mutex Ownership

A mutex has an owner: the thread that successfully locked it. That same thread must unlock it.

This ownership rule matches the usual critical-section structure:

Code must release the mutex along every path after a successful lock. Early returns are a common source of mistakes:

A C function can route exits through one cleanup point:

Languages with deterministic cleanup mechanisms commonly wrap mutex ownership in a scope-bound guard. The principle is the same: acquiring the mutex creates a responsibility that must be discharged exactly once.

Unlocking a mutex from a different thread is not a signaling mechanism. Depending on the mutex type and implementation, doing so is an error or undefined behavior.

Mutex Initialization and Lifetime

A statically allocated POSIX mutex can use:

A dynamically initialized mutex uses:

The NULL attribute pointer requests default attributes.

When the mutex is no longer needed:

Destroying a locked mutex, or a mutex that another thread may still use, is invalid. The containing object must therefore outlive every thread that can lock it.

A normal mutex is not automatically recursive. A thread that tries to lock a mutex it already owns may wait forever or encounter behavior determined by the configured mutex type. Restructuring the code so ownership is clear is usually safer than depending on recursive locking.

Keeping Critical Sections Deliberate

A mutex serializes every thread that uses it. The protected region should contain the complete invariant and no unrelated work.

Holding a mutex during a slow network call, file operation, or expensive computation can make other threads wait even when the shared-state update itself is short.

ScopeWhat happens inside the lock
Too broadUpdate state, make a network request, format the response, then unlock
NarrowerPrepare private work first, lock only to update shared state, unlock, then finish the private work

Narrower is not always correct. Moving one of several related updates outside the lock can break the invariant. Correctness determines the minimum critical section; measurement determines whether its cost needs attention.

When code needs several mutexes, the ownership relationships become more complex. The program must use a consistent acquisition policy rather than relying on timing.

Semaphores as Permit Counters

A semaphore maintains a nonnegative logical count of available permits.

Two operations define its behavior:

  • Wait, also called down or P: consume one permit, waiting if none is available.
  • Post, also called up or V: add one permit and allow a waiter to proceed if one exists.

For a semaphore initialized to 3:

Starting from 3 permits:

ActionPermits left
Thread A waits successfully2
Thread B waits successfully1
Thread C waits successfully0
Thread D waits0, so D must wait
Thread B postsD can consume the permit

The count represents capacity, not a protected application's numerical value. A semaphore initialized to the size of a resource pool can limit how many threads borrow resources simultaneously.

Using a POSIX Semaphore

An unnamed POSIX semaphore is declared with sem_t:

Initialize eight permits for use among threads in one process:

The second argument is 0, meaning the semaphore is shared among threads rather than configured for process-shared use.

Unlike the pthread mutex functions, these semaphore functions follow the conventional -1 and errno error-reporting style. sem_wait can be interrupted by signal delivery, so the example retries when errno is EINTR.

POSIX semaphore operations also provide memory synchronization. Ordinary data prepared before a post can be handed to a thread that proceeds through the corresponding wait when both sides follow a valid ownership protocol.

After all users have finished:

Destroying a semaphore while threads are waiting on it is invalid. Platform support for unnamed POSIX semaphores also varies; they are available on Linux, while some other POSIX environments require a different semaphore form.

Balanced Semaphore Accounting

Every successful wait consumes one permit. The code must eventually post exactly one permit when that use finishes:

A successful wait is followed by using one unit of capacity, and then a post.

An early return that skips sem_post leaks capacity. Enough leaks eventually reduce the usable pool to zero, causing all later waiters to stop making progress.

Posting without a corresponding use has the opposite effect. It inflates the count and can allow more concurrent users than the underlying resource supports.

The semaphore count should not be polled as a decision-making snapshot. Even if an interface reports that two permits appear available, other threads can consume them immediately. The wait operation itself is the atomic attempt to acquire capacity.

Binary Semaphores vs. Mutexes

A semaphore initialized to 1 is often called a binary semaphore:

A count of 1 means available, and a count of 0 means unavailable.

It can limit entry to one thread, but it does not gain mutex ownership semantics.

A thread can post a semaphore that another thread waited on. That is useful for signaling and resource accounting. It would violate the ownership contract of a mutex.

Use a mutex when the code means:

The thread that enters this critical section owns the responsibility to leave it.

Use a semaphore when the code means:

One unit of a countable resource is being consumed or produced.

Treating the two as interchangeable hides intent and removes useful ownership checks.

Predicate Waiting with Condition Variables

A condition variable lets a thread wait until shared state may satisfy a condition.

Examples of conditions include:

The condition variable does not contain the condition. The actual condition is a predicate evaluated over ordinary shared state.

A condition variable is used with a mutex:

The standard waiting pattern is:

The calling thread must own the mutex before calling pthread_cond_wait.

Concurrent waits on one condition variable must use the same mutex association. The predicate, mutex, and condition variable form one protocol.

Mutex Release During Waiting

A waiter cannot keep the mutex while waiting for another thread to change the protected state. The other thread would be unable to acquire the mutex and make the predicate true.

Condition-variable waiting therefore combines two operations:

pthread_cond_wait performs this transition atomically with respect to the condition-variable protocol. When it returns, it has reacquired the mutex before returning control to the caller.

Releasing the mutex and starting to wait happen as one step. If they did not, the publisher could slip between them and signal a condition nobody was waiting on yet.

Without the combined release-and-wait operation, a notification could occur in the gap:

The waiter missed the signal and may sleep even though the state is ready. The condition-variable API closes that vulnerable gap when both sides follow the mutex protocol.

Condition-Variable Wait Loops

The predicate must be checked with while, not if:

There are several reasons.

A condition wait may return spuriously, without a signal that makes the predicate true.

Another thread may consume or change the relevant state after the signal but before this waiter reacquires the mutex.

A broadcast may wake several threads even though the new state can satisfy only some of them.

The signal means “the state may have changed,” not “this particular thread now owns a true condition.” The mutex protects the recheck, and the loop makes the predicate the final authority.

Signaling and Broadcasting

After changing the protected state, a thread can wake waiters with:

This wakes at least one waiter if waiters exist. It is appropriate when one state change can enable one unit of waiting work.

To wake all current waiters:

Broadcasting is appropriate when a global state change may make the predicate true for many threads, such as configuration becoming permanently ready or shutdown being requested.

A reliable publication sequence is:

The state change, not the notification, is the lasting fact. A condition variable does not store a count of old signals. If no thread is waiting, pthread_cond_signal does not create a permit for a future waiter.

A future waiter remains correct because it checks the protected predicate before sleeping. If the state is already ready, it skips the wait.

Complete Condition-Variable Example

This program starts three workers that require a configuration version. The initial thread publishes the version and broadcasts the change.

Some workers may already be waiting when the broadcast occurs; others may start later. Both cases work because every worker checks the persistent predicate while holding the same mutex.

Compile and run it:

The worker lines can appear in any order:

The nondeterministic print order is harmless. The protected guarantee is that every worker copies version 7 only after configuration_ready becomes true.

Condition Variables and Mutex Roles

The mutex is required for more than the call to pthread_cond_wait. It makes the predicate check and the decision to wait one coordinated action.

The publishing thread must use the same protection when modifying state:

RoleSequence
WaiterLock, check the predicate, wait and reacquire, check again, unlock
PublisherLock, change the predicate, signal, unlock

Changing the predicate without the mutex can create a data race and can break the relationship between the state transition and notification.

The condition variable also does not protect arbitrary code after the mutex is unlocked. A waiter should copy or consume the protected state before unlocking, or establish another valid ownership rule for later use.

Semaphore or Condition Variable?

The difference is whether the synchronization state is the count itself or an arbitrary predicate.

A semaphore remembers permits:

A post with no waiter increases the count, so a future wait can still consume that permit.

A condition variable remembers no notifications:

A signal with no waiter stores nothing, so a future thread has to check the shared predicate itself.

Use a semaphore for countable capacity or completed units that should accumulate. Use a condition variable when threads must wait for a predicate over mutex-protected state.

For example, “eight database connections are available” maps naturally to semaphore permits. “The queue is nonempty and the service is not shutting down” is a compound predicate over shared state and maps naturally to a condition variable with a mutex.

Choosing the Primitive

Start from the state transition rather than the API name.

Use a mutex when one owner must preserve an invariant across a critical section. The protected region can include several fields and operations.

Use a semaphore when code consumes and returns units of a bounded capacity, or when produced units must accumulate until consumers claim them.

Use a condition variable with a mutex when a thread must wait for an arbitrary predicate to become true. The shared state stores the truth; the condition variable makes waiting efficient.

These primitives can appear together because they solve different aspects of a design. Their roles should remain distinct enough that a reader can explain what the mutex owns, what each permit represents, and which predicate every condition wait rechecks.

Loading simulation...

Summary

A mutex gives one thread ownership of a critical section and protects an invariant across ordinary shared-memory accesses. Every conflicting access must use the same mutex, and the owning thread must release it on every path.

A semaphore represents countable permits. Waiting consumes one permit, posting returns one, and the accounting must remain balanced. A binary semaphore can limit entry to one thread but does not have mutex ownership semantics.

A condition variable allows threads to wait for a predicate over mutex-protected state. pthread_cond_wait releases the mutex while waiting and reacquires it before returning. Waiters must always recheck the predicate in a loop, while publishers change the state under the mutex before signaling or broadcasting.

Quiz

Mutexes, Semaphores, and Condition Variables Quiz

5 quizzes