One is a lock with exactly one key and one owner. The other is a shared counter that any thread can nudge up or down.
Both a mutex and a semaphore are synchronization primitives, both get used to keep threads from stepping on each other, and in code they can look almost identical — acquire() on one side, release() on the other. But they encode two different ideas. A mutex is about ownership: it protects exactly one resource, and only the specific thread that locked it is allowed to unlock it. A semaphore is about counting: it has no concept of who did what, and any thread may increment or decrement it — which is exactly what you want when managing a pool of several interchangeable resources instead of exclusive access to a single one.
A mutex ("mutual exclusion" lock) guards a single shared resource and has exactly two states: locked or unlocked. Whichever thread calls lock() first becomes the owner and receives the one and only key. Every other thread that calls lock() has to wait until that same thread calls unlock() — and critically, the mutex remembers who the owner is, so it can (and typically does) reject an unlock attempt from a thread that never held the key. A semaphore guards a count instead of an identity. It starts at some value N — say, 5, for a pool of 5 database connections — and any thread can call wait() (decrement, "take a slot") or signal() (increment, "give a slot back"). The semaphore never checks which thread is calling either operation. It just tracks a number.
That rejection isn't a technicality — it's the entire point of a mutex. Because the lock remembers its owner, a well-implemented mutex can detect a thread trying to unlock something it never locked and refuse (or flag it as a bug). That same owner-tracking is also what lets some mutex implementations support priority inheritance— temporarily boosting the priority of the thread holding the lock so a higher-priority thread waiting on it isn't starved by a lower-priority one. None of that is possible without knowing exactly who's holding the key.
The reason these two primitives look similar in code but behave so differently in practice comes down to what each one is actually tracking. A mutex tracks who — it stores the identity of the thread that currently owns it, and it uses that identity to decide whether an unlock call is valid. That single design choice is what makes a mutex safe to reason about: exactly one thread is ever "inside" the protected section, and the system can enforce that no other thread can accidentally end it early. A semaphore tracks how many— it stores an integer, full stop. When the count represents a pool of five database connections, a print queue with three workers, or a rate limiter allowing ten concurrent requests, there's no single "owner" to track, because up to N threads are legitimately inside at once and any of them finishing should free up a slot for whoever's next. That's a fundamentally different job than guarding one resource, and it's why a semaphore is built without the ownership bookkeeping a mutex requires.
This one is close enough to be genuinely dangerous. It's true that a binary semaphore — one initialized to 1, where wait() and signal() are only ever called in matched pairs — can approximate mutual exclusion: at most one thread gets through at a time, the same as a mutex. But "approximate" is doing a lot of work in that sentence. A binary semaphore still has no concept of ownership, which means nothing stops a different thread than the one that called wait() from calling signal() — releasing a "lock" it never held. Sometimes that's intentional (a producer signaling a consumer is a legitimate, common pattern) — but when it's used to imitate mutual exclusion and a thread that isn't supposed to release does anyway, often due to a bug in error-handling or cleanup code, the count silently goes back to 1 while the "owning" thread is still mid-critical-section. Now two threads can both get past wait() at once, which is precisely the bug the lock was supposed to prevent — and it will look, from the logs, like mutual exclusion should have held. A real mutex closes this hole by design: it rejects an unlock from a non-owner instead of silently accepting it, and because it tracks an owner, it can additionally support features like priority inheritance that a semaphore structurally cannot. A binary semaphore can behave likea mutex when everyone uses it correctly. A mutex behaves like a mutex even when someone doesn't.
Explains the two most commonly confused synchronization primitives: a mutex, which is ownership-based and enforces that only the exact thread that acquired it may release it while guarding a single shared resource; and a semaphore, which is a counting signal with no ownership concept, where any thread may increment (signal) or decrement (wait) it, typically used to manage a pool of N interchangeable resources. Illustrated with a lock-and-key ownership diagram and a connection-pool counting diagram, plus why a binary semaphore is not a safe substitute for a real mutex.
Both are synchronization primitives with similar-looking APIs — acquire/release, lock/unlock, wait/signal — and a semaphore initialized to 1 can superficially behave like a mutex, so it is tempting to treat a mutex as a special case of a semaphore. But they encode different guarantees. A mutex enforces exclusive access to one resource and tracks which thread owns the lock, rejecting a release from any other thread. A semaphore tracks a count with no concept of identity at all — it manages how many units of a resource are available, not who is allowed to touch which unit.
A mutex starts unlocked. The first thread to call lock() becomes its owner and the mutex remembers that identity. Every other thread calling lock() blocks until the owner calls unlock() — and a correct implementation will refuse (or flag as an error) an unlock() call from any thread other than the current owner, because unlocking isn't just "set a bit back to zero," it's "the owner is done."
A semaphore starts at an initial count N representing N available units of some resource — for example, 5 slots in a connection pool. Any thread can call wait() (also called acquire or P), which blocks if the count is 0 and otherwise decrements it, and any thread can call signal() (also called release or V), which increments the count and wakes a waiting thread if one exists. The semaphore performs no identity check on either call: it doesn't know or care which thread is calling, only what the current count is.
Reach for a mutex whenever you have one shared resource — a single data structure, a single file handle, a single critical section — that only one thread should touch at a time, and where "the thread that locked it should be the one to unlock it" is a real invariant you want enforced. Reach for a semaphore whenever you're managing a pool of N interchangeable resources — database connection pools, worker thread pools, a fixed number of API rate-limit slots — where up to N callers can legitimately be active simultaneously and which specific caller frees a slot doesn't matter, only that the count stays accurate. Using a semaphore to simulate a mutex (a "binary semaphore") works only as long as every caller is disciplined about calling wait() and signal() in matched pairs from the same thread; the moment cleanup code, error-handling, or a different thread calls signal() out of turn, the count can drift and let two threads into a section meant to be exclusive.
Not quite, even though a semaphore initialized to 1 can approximate one. The difference is ownership: a mutex tracks which thread locked it and will reject an unlock call from any other thread, while a binary semaphore has no such check — any thread can call signal() regardless of which thread called wait(). That missing check is exactly what lets a binary semaphore silently fail to provide mutual exclusion if used incorrectly.
Yes, in the specific form of a binary semaphore (initialized to 1, with wait()/signal() called only in matched pairs). It can work correctly, but it provides no structural guarantee against a bug where a different thread than the one that acquired it releases it — a mistake a real mutex is designed to prevent.
Priority inheritance works by temporarily raising the priority of whichever thread currently holds a lock, so a higher-priority thread waiting on it isn't stuck behind a lower-priority one. That requires knowing exactly which thread holds the lock right now — information a mutex tracks by design. A semaphore only tracks a count, with no record of which thread(s) currently hold a "unit," so there is no single owner whose priority could be boosted.
They are the same thing — "semaphore" and "counting semaphore" are typically used interchangeably to mean a semaphore whose count can range across multiple values (for example, 0 through 5 for a 5-slot pool). The term "binary semaphore" refers specifically to a semaphore restricted to just 0 and 1, which is the configuration sometimes used to approximate a mutex.
Use a semaphore when more than one thread is allowed to be "inside" at the same time — for example, capping concurrent access to a pool of N interchangeable resources such as database connections, worker threads, or API rate-limit slots. Use a mutex when exactly one thread should have access at a time and you want the system to enforce that only the thread that acquired the lock can release it.
Try our Software Engineering & Cloud Studio
More calculators, simulators, and guides for this discipline.