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.
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, writepthread_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 exitA 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?+
Can a semaphore be released by a different thread than the one that acquired it?+
Why use a semaphore instead of just a mutex around a resource pool?+
Related reading
2026-06-04
ACID vs BASE: What Database Guarantees Actually Promise
ACID and BASE describe two ends of a tradeoff between strict correctness and scalable availability. Learn what each guarantee means, when each fits, and why most modern databases sit somewhere in between.
2026-06-04
Big-Endian vs Little-Endian
Byte order explained: how big-endian and little-endian lay out multi-byte numbers in memory, why network protocols pick one, and when the difference actually bites you.
2026-06-04
Big-O Notation in Plain English
Big-O describes how an algorithm's runtime or memory grows as input grows. Learn the common classes — O(1), O(log n), O(n), O(n log n), O(n^2), O(2^n) — with plain examples.
2026-06-04
CORS in Plain English: Why the Browser Blocks Your Fetch
A clear walkthrough of CORS and the same-origin policy — what an origin is, why your fetch fails, how servers opt in, and the big misconception about who CORS actually protects.
2026-06-04
Environment Variables and PATH, Explained
What environment variables actually are, why they hold config and secrets, and how PATH decides which binary runs when you type a command.
Get the best tools, weekly
One email every Friday. No spam, unsubscribe anytime.