← Back to CS Fundamentals

Mutex vs Semaphore

CS FundamentalsMidconcurrency

The Question

What is the difference between a mutex and a semaphore?

What a Strong Answer Covers

  • mutex has ownership
  • "semaphore is a counter
  • no ownership
  • use case for each

Senior-Level Answer

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.

Key Differences

AspectMutexSemaphore
OwnershipYes — only acquiring thread can releaseNo — any thread can release
CountBinary (0 or 1)Integer N (≥0)
Primary useProtecting a single critical sectionLimiting access to N resources or signaling
SignalingNot suited for signaling between threadsIdeal for producer-consumer signaling (init to 0)
Deadlock detectionPossible (OS/runtime tracks ownership)Not possible (no ownership)
Priority inheritanceSupported in some implementationsNot supported
Binary variantMutex is inherently binaryBinary semaphore (N=1) approximates but lacks ownership

What Separates a 2/3 from a 3/3

2/3 — Passing but Incomplete

Correctly distinguishes ownership semantics (mutex is owned, semaphore is not), explains counting semaphore for N resources, and gives concrete use cases for each.

3/3 — Strong Answer

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.

Common Mistakes

  • Saying a binary semaphore and a mutex are equivalent — they differ in ownership, which has meaningful implications for safety and diagnostics.
  • Not knowing the signaling pattern for semaphores — using a semaphore initialized to 0 for event signaling is a distinct and important use case separate from resource limiting.
  • Confusing mutex with monitor — a monitor combines a mutex with a condition variable for wait/notify patterns.
  • Not knowing Python's specific primitives (threading.Lock, asyncio.Lock, threading.Semaphore) when interviewed for a Python role.

Follow-Up Questions

  • How would you implement a connection pool using a semaphore? — Initialize semaphore to pool size. Each borrow acquires it (blocks at 0). Each return releases it. The semaphore prevents over-borrowing without tracking which connections are in use.
  • What is a deadlock and how can mutexes cause one? — Thread A holds mutex 1, waits for mutex 2. Thread B holds mutex 2, waits for mutex 1. Neither can proceed. Prevention: consistent lock ordering, timeout-based acquisition, or lock-free algorithms.
  • What is a condition variable and how does it differ from a semaphore? — Condition variables are used with a mutex: release the mutex and sleep atomically until notified, then reacquire the mutex. More flexible for complex wait conditions than a semaphore.
  • In Python's asyncio, why do you need asyncio.Lock instead of threading.Lock? — threading.Lock blocks the OS thread, blocking the entire event loop. asyncio.Lock suspends only the coroutine, yielding control back to the event loop.

Related Questions

  • Process vs Thread
  • Race Condition
  • Deadlock — 4 Conditions, Prevention
  • Thread Safety — Is dict Thread Safe?
  • HashMap Internals

Can You Explain This Cold?

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
GrindQuestionsAITechnical interview assessment
TermsPrivacyAbout