Skip to main content

Mutex vs. Semaphore: Concurrency Primitives & Tradeoffs

Medium

Interview Question: "What is the difference between a mutex and a semaphore? What is a spinlock, how does ownership impact priority inheritance, and when do you use each?"

Both a mutex and a semaphore control access to a shared resource. The difference comes down to one question: does it track who's holding it?


The analogy: a bathroom key vs. a nightclub bouncer​

  • A mutex is a single-occupancy bathroom key. A coffee shop has one bathroom. You take the key, go in, lock the door. You own that key until you come back out and hand it over. If someone else tries to hand back a "duplicate" key while you're still inside, the barista rejects it — you're the only one allowed to release it.
  • A semaphore is a bouncer with a clicker, capacity 3. Three people go in, the clicker hits 0. A fourth person has to wait. Someone leaves through the side door, the clicker ticks back up to 1, and the next person in line gets in. Nobody "owns" a slot — anyone leaving frees one up for anyone waiting.
Mutex
locked by Thread A
Semaphore
count = 3

The technical version​

Mutex (mutual exclusion):

  • A lock that lets exactly one thread into a critical section at a time.
  • Internally just a boolean — locked or unlocked.
  • Strict ownership: only the thread that called lock() is allowed to call unlock(). Anyone else trying gets an error.

Counting semaphore:

  • A signaling tool built on an integer counter (≥ 0).
  • Two operations: wait() / P() decrements the counter (and blocks if it hits below 0), signal() / V() increments it (and wakes someone up if anyone's waiting).
  • No ownership at all — Thread A can call wait() and a completely unrelated Thread B can call signal().

Two things interviewers love to probe​

1. Mutex vs. spinlock — sleeping vs. busy-waiting. A mutex, when blocked, puts the waiting thread to sleep and lets the CPU run something else — cheap in CPU time, but costs a few microseconds to wake back up. A spinlock just loops, checking the lock over and over, burning 100% CPU the whole time. Spinlocks only make sense for extremely short critical sections, like inside a kernel interrupt handler where sleeping isn't even an option.

2. Why only mutexes support priority inheritance. If a low-priority thread is holding a lock a high-priority thread needs, the OS can temporarily boost the low-priority thread so it finishes fast and gets out of the way — that's priority inheritance. It only works for a mutex, because the kernel knows exactly which thread owns the lock. A semaphore has no concept of an owner, so there's no one specific thread to boost.


Comparison​

DimensionMutexCounting Semaphore
What it isA lockA signal/counter
StateLocked / unlockedAn integer ≥ 0
OwnershipStrict — only the locker can unlockNone — anyone can signal
Priority inheritanceSupportedNot possible
Used forProtecting a critical sectionRate-limiting, resource pools, handoffs
Typical caseGuarding a shared data structureCapping a DB connection pool at 10

Say this in the interview​

"A mutex is a lock with strict ownership — only the thread that locked it can unlock it, which is also why it can support priority inheritance. A semaphore is a counter for signaling with no ownership at all, which makes it a better fit for things like limiting how many threads can use a resource pool at once."


Code: mutex vs. counting semaphore​

#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>
#include <unistd.h>

sem_t pool_semaphore;

void* pool_worker(void* arg) {
long id = (long)arg;

// Decrement semaphore counter (wait if pool is full)
sem_wait(&pool_semaphore);
printf("[Worker %ld] Acquired slot in pool\n", id);
sleep(1); // Simulate work using resource
printf("[Worker %ld] Releasing slot\n", id);

// Increment semaphore counter
sem_post(&pool_semaphore);
return NULL;
}

int main() {
pthread_t threads[5];
// Initialize counting semaphore with capacity of 2
sem_init(&pool_semaphore, 0, 2);

for (long i = 0; i < 5; i++) {
pthread_create(&threads[i], NULL, pool_worker, (void*)i);
}

for (int i = 0; i < 5; i++) {
pthread_join(threads[i], NULL);
}

sem_destroy(&pool_semaphore);
return 0;
}