A mutex is owned by exactly one thread and must be released by the thread that acquired it. A semaphore is a counter with no ownership: any thread can signal it and any thread can wait on it. Using a mutex where a semaphore is needed, or vice versa, does not just produce suboptimal code. It can produce deadlocks that never trigger in testing and only appear under specific production load patterns.
Pithy Cyborg | AI FAQs – The Details
Question: What is the difference between a mutex and a semaphore. And when does using the wrong one deadlock your program?
Asked by: Claude Sonnet 4.6
Answered by: Mike D (MrComputerScience)
From Pithy Cyborg | AI News Made Simple
And Pithy Security | Cybersecurity News
The Ownership Distinction That Changes Everything
A mutex (mutual exclusion lock) enforces exclusive access to a resource. The defining property is ownership: the thread that acquires the mutex is the only thread allowed to release it. Most mutex implementations enforce this at the OS level and will panic or return an error if a different thread attempts to unlock a mutex it does not own. This ownership semantics enables priority inheritance, a mechanism where a high-priority thread waiting on a mutex temporarily donates its priority to the low-priority thread holding it, preventing priority inversion in real-time systems.
A semaphore is a non-negative integer counter with two atomic operations: wait (decrement, block if zero) and signal (increment, wake a waiting thread). There is no concept of ownership. Thread A can wait on a semaphore and Thread B can signal it. This cross-thread signaling is not a bug or a misuse. It is the fundamental design. A binary semaphore initialized to 1 looks superficially like a mutex but lacks ownership semantics, which makes it behave differently in subtle and important ways.
The practical consequence: use a mutex when you are protecting a resource that a single thread must access exclusively. Use a semaphore when you are signaling between threads, controlling access to a pool of N identical resources, or coordinating producer-consumer workflows where the producer and consumer are different threads.
Getting this wrong does not always cause a deadlock immediately. Sometimes it causes a priority inversion that degrades latency. Sometimes it causes a double-release that corrupts internal OS state. Sometimes it causes a deadlock only when a specific thread scheduling pattern occurs under load, which is why the bug survives testing and surfaces at 3am in production.
Three Specific Scenarios Where the Wrong Choice Deadlocks You
Understanding the failure modes concretely is more useful than abstract rules.
The first scenario is recursive locking with a non-recursive mutex. A thread acquires a mutex and then calls a function that attempts to acquire the same mutex. With a non-recursive mutex, the thread deadlocks on itself immediately. This is not a rare mistake. It happens routinely when refactoring code where a function that used to be called only from unlocked contexts gets called from a locked context. The fix is either a recursive mutex, restructuring to avoid reentrant locking, or extracting the locked logic into a helper that does not acquire the lock. Using a semaphore initialized to 1 instead of a mutex makes this worse: a semaphore has no ownership, so a recursive wait genuinely blocks rather than potentially returning an error, and the deadlock is harder to diagnose.
The second scenario is releasing a mutex from the wrong thread. Consider a thread pool where Thread A acquires a mutex to start a long operation and Thread B is supposed to release it when the operation completes. With a proper mutex, the release attempt from Thread B will fail because Thread B does not own the mutex. This failure is a signal that the design is wrong. If the developer works around this by using a binary semaphore instead, the release succeeds, but the ownership invariant is gone. Now priority inheritance cannot function, and any error recovery path that tries to clean up the semaphore from the acquiring thread will deadlock or corrupt state.
The third scenario is a producer-consumer queue implemented with a mutex instead of a semaphore for the item count. A consumer thread acquires a mutex to check whether the queue is empty, finds it empty, and waits. The producer cannot acquire the mutex to add an item because the consumer holds it. Neither thread can proceed. EDR detecting ctypes shellcode involves a structurally identical coordination pattern between detection threads and response threads where incorrect synchronization primitive choice causes the detector to block the very signal it is waiting for.
The Correct Primitive for Common Synchronization Patterns
Knowing which primitive fits which pattern eliminates most of these failure modes before they reach code review.
For protecting shared data structures accessed by multiple threads, use a mutex. Acquire before access, release after. std::mutex in C++, pthread_mutex_t in C, sync.Mutex in Go, and threading.Lock in Python all implement this pattern with ownership enforcement.
For signaling between threads that an event has occurred, use a semaphore or a condition variable. A condition variable (condvar) is the preferred modern choice for complex signaling because it integrates with a mutex to eliminate the race between checking a condition and waiting on it. std::condition_variable in C++, sync.Cond in Go, and threading.Condition in Python all implement this. A binary semaphore works for simple one-shot signaling but condition variables handle spurious wakeups and complex predicates more cleanly.
For controlling access to a pool of N identical resources such as database connections, thread pool slots, or rate-limited API tokens, use a counting semaphore initialized to N. Each acquisition decrements the counter. Each release increments it. When the counter reaches zero, new acquisitions block until a resource is returned. std::counting_semaphore in C++20, semaphore.Semaphore in Python, and sync.WaitGroup (for the special case of waiting for N goroutines) in Go are the idiomatic choices.
For coordinating a set of threads to all reach a barrier before any proceeds, use std::barrier in C++20, CyclicBarrier in Java, or a counting semaphore with a specific initialization pattern. Do not use a mutex for this. The barrier pattern requires cross-thread signaling that mutex ownership semantics explicitly prohibit.
What This Means For You
- Always ask whether ownership matters before choosing a primitive. If the thread that acquires must be the thread that releases, use a mutex; if different threads signal and wait, use a semaphore or condition variable.
- Never use a binary semaphore as a drop-in mutex replacement. The missing ownership semantics silently disable priority inheritance and make recursive locking and error recovery subtly broken.
- Use condition variables instead of semaphores for producer-consumer patterns in C++, Java, and Go. they handle spurious wakeups correctly and integrate with the associated mutex to close the check-then-wait race condition.
- Run your concurrent code under helgrind or ThreadSanitizer to detect lock ordering violations and missing synchronization. These tools catch the wrong-primitive class of bugs far more reliably than code review alone.
- Document every synchronization primitive’s ownership contract explicitly. A comment stating “must be held by the thread performing X, released by the same thread” prevents the refactoring accidents that introduce wrong-thread-release deadlocks in long-lived codebases.
Pithy Cyborg | AI News Made Simple
Subscribe (Free): https://pithycyborg.substack.com/subscribe
Read archives (Free): https://pithycyborg.substack.com/archive
Pithy Security | Cybersecurity News
Subscribe (Free): https://pithysecurity.substack.com/subscribe
Read archives (Free): https://pithysecurity.substack.com/archive
