Skip to the content
Software Made Clear Diagrams that show the mechanism About

Mutex vs semaphore

ANSWER

A mutex protects one thing and belongs to whoever took it; a semaphore counts permits and belongs to nobody. Use the first for exclusive access and the second to cap how many may proceed at once — swapping them is how ownership bugs start.

IN PLAIN TERMS

A mutex works like a single toilet key, a semaphore like the counter at a car park barrier. The key comes back from the person who took it. The barrier only knows how many spaces are left, and has no idea who parked in them.

Somewhere in whatever got someone to this page is the observation that a mutex is a semaphore initialised to one, and the observation is not wrong — a semaphore started at one really does enforce exclusion in code that acquires and releases it correctly. What that framing leaves unanswered is why a second name would ever be needed for the same mechanism. The count is not the difference. Ownership is: a mutex is built around the idea that whoever locked it is the one expected to unlock it, and a semaphore was never built around that idea at all. Everything else in this comparison follows from that one fact.

Ownership is the difference#

A mutex has an owner. The thread that locks it is the thread an implementation expects to see calling unlock, and that expectation is what makes several other things possible at all — a runtime that knows who holds a mutex can check whether the thread releasing it is the right one, can let that same thread acquire it again without deadlocking itself against its own lock, and can lend that thread a temporary priority boost when a more urgent thread is waiting on it. Each of those depends on knowing who the owner is, and a semaphore, which records no owner, offers none of them.

What a given implementation actually does with that knowledge differs, and naming the difference matters more here than in most comparisons, because the mistake underneath is the same everywhere and the consequence is not. POSIX threads distinguish by mutex type: unlocking a normal mutex from the wrong thread is undefined behaviour, while an error-checking or recursive mutex is required to fail instead, returning EPERM. C++’s std::mutex documents the same undefined-behaviour outcome for unlock by a non-owning thread, with no error-checking variant built into the type itself. Java’s ReentrantLock goes further and actively enforces the rule: a thread that never held the lock calling unlock gets an IllegalMonitorStateException, not silent damage. The split is not arbitrary: POSIX’s error-checking and recursive types, and Java’s ReentrantLock, track the owner and can therefore reject the wrong thread outright, which is why the mistake surfaces as a returned error or a thrown exception in each. POSIX’s normal type and C++’s std::mutex decline to pay for that bookkeeping, and that is exactly why the same mistake is left undefined there instead of caught.

Recursive acquisition splits along similarly language-specific lines, and the shape of the split is worth seeing because it is not the same shape twice. C++ makes it a separate type: relocking a std::mutex from the thread that already holds it is itself undefined behaviour and may deadlock that thread against itself, so std::recursive_mutex exists as its own type specifically to allow the same thread back in. POSIX takes the opposite shape — recursion is an attribute of the one mutex type rather than a second type, set through pthread_mutexattr_settype on an ordinary mutex before it is created. Java folds recursion into its own locking model too, though by two separate mechanisms rather than one: a thread already inside a synchronized block on an object may re-enter that same block, and, independently, a thread already holding a ReentrantLock may re-acquire it, with ReentrantLock alone tracking that recursion as an explicit hold count rather than a single locked flag. Whether recursion is a property, an attribute, or a separate type is a decision each implementation made on its own. Priority inheritance is narrower still — it is POSIX’s own answer to priority inversion, reached by setting a mutex’s protocol attribute to PTHREAD_PRIO_INHERIT so a thread blocking on it lends the holder its own priority until the mutex is released, and it is only possible because the mutex already knows who that holder is. A semaphore has nobody to lend priority to.

But a binary semaphore looks identical#

Take the objection seriously, because it is the search phrase that brought someone here. A semaphore initialised to one does enforce mutual exclusion in code that uses it correctly, and the calling code can read exactly like a lock and unlock pair. What differs is not visible on the page, and the documentation for more than one implementation says so directly rather than leaving it to be inferred. Java’s own Semaphore documentation gives semaphore-as-mutex the name binary semaphore and states plainly that, used this way, the lock can still be released by a thread other than the owner, because semaphores have no notion of ownership at all. C++’s counting_semaphore inherits the identical shape: unlike std::mutex, it is not tied to threads of execution, and acquiring and releasing may legitimately happen on different threads. Python’s Semaphore and POSIX’s own sem_t track a counter only, never a holder.

That absence is not a defect sitting quietly inside semaphores waiting to bite — it is what makes a signalling use natural for a semaphore and a misuse for a mutex. One thread finishing setup and a different thread being allowed to proceed is exactly the release-from-somewhere-else shape a semaphore was built for. Forcing that same handoff through a mutex means unlocking from a thread that never locked it, which the previous section already named as either undefined or actively rejected depending on the implementation underneath. The practical rule follows directly: if the two operations happen in different threads, reach for a semaphore; if they bracket a critical section inside a single thread, reach for a mutex. The habit this comparison keeps relying on — checking what a term actually promises instead of trusting that a familiar shape carries a familiar guarantee — is the same habit trees, hashing and traversal get put through under this site’s foundations pillar, one structure at a time, on ground that has nothing to do with locking.

A mutex used as a signal#

The shape: one thread locks a mutex to mark that some work has started, and a different thread is expected to unlock it once that work is ready — a producer signalling a consumer, a setup routine handing off to a worker, a flag wearing a lock’s clothing. It runs cleanly enough on a quiet machine during development that nobody questions the design until real load changes what “cleanly enough” means.

What it costs depends on which implementation sits underneath, and neither answer is good. On a runtime that checks ownership strictly, the unlock from the wrong thread throws or fails with an error the surrounding code was never written to handle, so the failure at least announces itself, loudly and at the worst moment. On one that does not check, the unlock simply succeeds, and the mutex now protects nothing at all — any thread can walk through it, because the ownership discipline the design was quietly leaning on was never actually enforced. Either way the code reads as though it is protecting shared data, when what it is actually doing is coordinating between threads, and coordination was never the job a mutex signed up for.

The tell shows up in the shape of the code before it ever fails at runtime: an unlock call living in a different function, or a different thread’s own path, from the lock it is meant to pair with, and a lock held across a wait for some external condition instead of around the handful of statements it was meant to protect. Neither alone proves anything is wrong. Together they are exactly this pattern, sitting quietly until a thread schedule finally exposes it.

A mutex misused this way is not one small fix away from correct. The tool for “wait until something is ready” is a condition variable, paired with the mutex that protects the condition being waited on, or a semaphore used for precisely the signal it was built to send. Naming which of those the design actually needed is the fix — not finding a way to make the mutex hold a job it was never built to do. Neither of these two primitives has anything to say once the two sides needing coordination are on separate machines instead of separate threads inside one process — there is no shared memory left to lock or count against at all. The distributed-systems path on this site names that harder problem directly, and every one of its nodes today is a title with no article written behind it.

Situation Take Because
One thread at a time may touch this data Mutex Ownership is what you are expressing
At most N may use a shared resource Semaphore The count is the whole point
One thread signals another that work is ready Semaphore Release from a different thread is legal
The lock and unlock bracket one function Mutex The runtime can check the owner

IF YOU REMEMBER ONE THING

The count is not the difference; ownership is. A mutex expects the thread that locked it to be the thread that unlocks it, and everything a mutex can do that a semaphore cannot — reject the wrong thread, let the same one back in, lend it a priority — rests on knowing who the holder is.

Questions people also ask

5 QUESTIONS
Is a mutex just a semaphore set to one?

A semaphore initialised to one does enforce the same mutual exclusion, and the code can look identical. What it never gains is ownership: nothing records which thread signalled it, so a release from a thread that never acquired it is legal rather than a bug. The count matches; the guarantee behind it does not.

Can another thread unlock a mutex?

Implementations disagree, and each reacts differently to the same mistake. POSIX's error-checking and recursive mutex types fail with EPERM; its normal type leaves the outcome undefined. C++'s std::mutex documents unlock by a non-owning thread as undefined behaviour outright. Java's ReentrantLock checks and throws IllegalMonitorStateException instead.

What is a binary semaphore for?

Signalling between threads rather than protecting data inside one — a thread finishing setup can release it for a different thread to acquire, a handoff a mutex's ownership rules forbid. Java's own documentation names this pattern directly: a semaphore used this way behaves like a lock nobody actually owns.

What is priority inversion?

A lower-priority thread holds a lock a higher-priority thread is waiting on, while a medium-priority thread that needs neither keeps running instead of either of them, because the scheduler only sees priority, not who is blocking whom. POSIX's PTHREAD_PRIO_INHERIT protocol addresses it by lending the holder the waiting thread's priority until release.

When should I use a condition variable instead?

Whenever a thread needs to wait for some condition to become true rather than for exclusive access to data — the two are different problems that look alike once a mutex gets misused to solve the second one. A condition variable is built to be waited on; a mutex paired with it should only guard the condition, never the waiting itself.