What is the difference between a mutex and a semaphore?
A mutex (mutual exclusion lock) and a semaphore are both synchronization primitives, but they model different problems and have different ownership semantics.
A mutex is a binary lock that allows exactly one thread to hold it at a time. Its defining characteristic is ownership: only the thread that acquired the mutex can release it. This models the exclusive access pattern — one critical section, one thread. If thread A holds a mutex and thread B tries to acquire it, thread B blocks until thread A releases it. Mutexes are used to protect shared data structures, file handles, or any resource that can't be safely accessed concurrently.
A semaphore is a counter-based primitive initialized to N. Each `acquire()` decrements the counter; each `release()` increments it. When the counter reaches 0, further `acquire()` calls block. Critically, semaphores have no ownership: any thread can call `release()`, even one that never called `acquire()`. A binary semaphore (N=1) looks like a mutex but lacks ownership enforcement — this is a meaningful difference.
Counting semaphores model limited resource pools: N database connections, N worker slots, N permits to rate-limit throughput. A thread acquires a semaphore to take a slot and releases it when done. Python's `threading.Semaphore(n)` and `asyncio.Semaphore(n)` implement this directly.
A signaling semaphore (initialized to 0) is used for thread coordination: one thread calls `acquire()` (and blocks), another calls `release()` to signal that an event has occurred. This is fundamentally different from mutual exclusion — it's about ordering, not exclusive access.
The ownership distinction has safety implications. A mutex with ownership allows deadlock detection (the OS or runtime knows which thread holds which mutex) and enables priority inheritance (elevating a low-priority thread holding a needed mutex). A semaphore cannot support these because any thread can release it.
In Python's asyncio, `asyncio.Lock()` is the coroutine-safe mutex equivalent and `asyncio.Semaphore(n)` is the counting primitive. In the threading module, `threading.Lock()` and `threading.Semaphore()` are the synchronous counterparts.
| Aspect | Mutex | Semaphore |
|---|---|---|
| Ownership | Yes — only acquiring thread can release | No — any thread can release |
| Count | Binary (0 or 1) | Integer N (≥0) |
| Primary use | Protecting a single critical section | Limiting access to N resources or signaling |
| Signaling | Not suited for signaling between threads | Ideal for producer-consumer signaling (init to 0) |
| Deadlock detection | Possible (OS/runtime tracks ownership) | Not possible (no ownership) |
| Priority inheritance | Supported in some implementations | Not supported |
| Binary variant | Mutex is inherently binary | Binary semaphore (N=1) approximates but lacks ownership |
Correctly distinguishes ownership semantics (mutex is owned, semaphore is not), explains counting semaphore for N resources, and gives concrete use cases for each.
All of the above plus: explains the signaling use case for semaphore (initialized to 0 for event synchronization), discusses why ownership matters for deadlock detection and priority inheritance, and correctly notes that a binary semaphore is not equivalent to a mutex.
Reading the answer is step one. Explaining it unprompted — under interview pressure — is what actually matters. Get AI-graded feedback on your answer with follow-up probes on your weak points.
Get Graded — Free Assessment