pickuma.
Dev Knowledge

Mutex vs Semaphore: What Each One Guards

A mutex enforces exclusive access to a critical section; a semaphore is a permit counter that allows up to N holders. Here is when to reach for each.

5 min read

Both a mutex and a semaphore are synchronization primitives that keep concurrent threads from stepping on each other. They look similar in an API — you acquire, you release — but they answer two different questions: “may I be the only one here?” versus “is there a free slot for me?”

A Mutex Is About Exclusivity

A mutex (short for mutual exclusion) protects a critical section: a stretch of code that touches shared state which must not be modified by two threads at once. While one thread holds the mutex, every other thread that tries to lock it blocks until the holder releases.

The defining trait is ownership. A mutex has an owner — the thread that locked it — and conceptually only that thread is meant to unlock it. This ownership enables useful behavior: a recursive (reentrant) mutex lets its owner lock again without deadlocking, and some implementations can detect priority inversion and apply priority inheritance, temporarily boosting a low-priority holder so a high-priority waiter is not starved.

The classic use is protecting shared data. Imagine a counter incremented from many threads:

pthread_mutex_lock(&m);
counter += 1; // critical section: read, add, write
pthread_mutex_unlock(&m);

Without the lock, counter += 1 is not atomic — two threads can read the same value, both add one, and both write back, losing an increment. The mutex serializes the section so each increment happens cleanly.

A Semaphore Is a Permit Counter

A semaphore is just an integer counter with two atomic operations, historically called P (wait/acquire) and V (signal/release). Acquire decrements the count; if the count would go below zero the caller blocks. Release increments the count, potentially waking a waiter. There is no concept of ownership — the count is the whole state.

That counter is what makes a semaphore flexible. Initialize it to N and you allow up to N threads through at once. This is ideal for guarding a pool of interchangeable resources: say you have five database connections and want at most five threads using them concurrently.

pool = threading.Semaphore(5) # at most 5 concurrent holders
with pool: # acquire: blocks if 5 are already in use
conn = get_connection()
run_query(conn)
release_connection(conn)
# release happens on block exit

A semaphore initialized to 1 is a binary semaphore, which looks like a mutex but is not one — it still has no owner.

The other major use is signaling between threads. In a producer-consumer setup, a producer adds an item to a buffer and releases a semaphore; a consumer acquires it before taking an item, so it sleeps when the buffer is empty and wakes the instant something arrives. Here the two operations are deliberately performed by different threads — the producer signals, the consumer waits.

Choosing Between Them

Ask what you are actually modeling. If the answer is “this resource has exactly one user at a time, and the user manages its own lifecycle,” reach for a mutex — you get ownership, often reentrancy, and sometimes priority-inheritance safety. If the answer is “I have N equivalent slots” or “one party needs to wake another,” reach for a semaphore.

A common mistake is using a binary semaphore where a mutex belongs. It compiles, it usually works, but you lose ownership semantics: nothing stops an unrelated thread from releasing the permit, accidental double-releases push the count above one, and you forfeit priority inheritance. When the intent is mutual exclusion, name it with the primitive that means mutual exclusion.

FAQ

Is a binary semaphore the same as a mutex?+
No. Both allow one holder at a time, but a mutex has ownership — the locking thread is the one expected to unlock — while a binary semaphore is an ownerless counter capped at one. A mutex can also offer reentrancy and priority inheritance, which a semaphore does not.
Can a semaphore be released by a different thread than the one that acquired it?+
Yes, and that is often intentional. In producer-consumer signaling, the producer releases permits and the consumer acquires them. Because a semaphore tracks only a count with no owner, cross-thread acquire and release is fully valid.
Why use a semaphore instead of just a mutex around a resource pool?+
A mutex lets only one thread into the protected region at a time, which would serialize access to your whole pool. A semaphore initialized to N lets up to N threads run concurrently, so all N pooled resources can be in use at once while the (N+1)th caller waits.

Related reading

See all Dev Knowledge articles →

Get the best tools, weekly

One email every Friday. No spam, unsubscribe anytime.