Concurrency and Synchronization
When multiple threads access shared data without coordination, the result is a race condition — the outcome depends on unpredictable timing. Synchronization primitives (mutexes, semaphores, condition variables) impose ordering to prevent this.
Why It Matters
Any multi-threaded program — web servers, databases, game engines — must handle shared state correctly. Race conditions are the hardest bugs to reproduce and debug because they depend on scheduling timing. Understanding sync primitives and deadlock is non-negotiable for systems work.
The Problem: Race Conditions
int counter = 0;
// Thread 1 // Thread 2
// load counter → 0 // load counter → 0
// add 1 → 1 // add 1 → 1
// store → counter = 1 // store → counter = 1
// Expected: 2, Got: 1The read-modify-write is not atomic. Two threads interleave at the instruction level.
Synchronization Primitives
Mutex (Mutual Exclusion)
Only one thread can hold the lock at a time. Others block until it’s released.
#include <pthread.h>
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
int counter = 0;
void *increment(void *arg) {
for (int i = 0; i < 1000000; i++) {
pthread_mutex_lock(&lock);
counter++; // safe: only one thread at a time
pthread_mutex_unlock(&lock);
}
return NULL;
}Semaphore
A counter that allows up to N concurrent accessors. Mutex is a semaphore with N=1.
#include <semaphore.h>
sem_t sem;
sem_init(&sem, 0, 3); // allow 3 concurrent threads
sem_wait(&sem); // decrement (blocks if count == 0)
// critical section
sem_post(&sem); // incrementSpinlock
Like a mutex but busy-waits instead of sleeping. Good when hold time is very short (nanoseconds) and context switch cost exceeds spin cost.
pthread_spinlock_t slock;
pthread_spin_init(&slock, 0);
pthread_spin_lock(&slock); // busy-wait loop
// very short critical section
pthread_spin_unlock(&slock);Comparison
| Primitive | Blocking | Best For |
|---|---|---|
| Mutex | Sleeps (yields CPU) | General purpose, longer critical sections |
| Spinlock | Busy-waits | Very short critical sections, real-time |
| Semaphore | Sleeps | Resource counting (connection pools, etc.) |
| rwlock | Sleeps | Many readers, few writers |
Condition Variables
Wait for a condition to become true, without busy-waiting. Always paired with a mutex.
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int ready = 0;
// Consumer — waits for data
pthread_mutex_lock(&lock);
while (!ready) // MUST be while, not if (spurious wakeups)
pthread_cond_wait(&cond, &lock); // atomically: unlock + sleep + re-lock on wake
// use the data
pthread_mutex_unlock(&lock);
// Producer — signals data is ready
pthread_mutex_lock(&lock);
ready = 1;
pthread_cond_signal(&cond); // wake one waiter (broadcast wakes all)
pthread_mutex_unlock(&lock);Producer-Consumer Pattern
#define BUFSIZE 16
int buffer[BUFSIZE];
int count = 0;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t not_empty = PTHREAD_COND_INITIALIZER;
pthread_cond_t not_full = PTHREAD_COND_INITIALIZER;
void produce(int item) {
pthread_mutex_lock(&lock);
while (count == BUFSIZE)
pthread_cond_wait(¬_full, &lock);
buffer[count++] = item;
pthread_cond_signal(¬_empty);
pthread_mutex_unlock(&lock);
}
int consume(void) {
pthread_mutex_lock(&lock);
while (count == 0)
pthread_cond_wait(¬_empty, &lock);
int item = buffer[--count];
pthread_cond_signal(¬_full);
pthread_mutex_unlock(&lock);
return item;
}Deadlock
Four conditions (all must hold simultaneously):
| Condition | Meaning |
|---|---|
| Mutual exclusion | Resource can only be held by one thread |
| Hold and wait | Thread holds one lock while waiting for another |
| No preemption | Locks can’t be forcibly taken |
| Circular wait | A → waits for B → waits for A |
Prevention: break any one condition. The most practical:
- Lock ordering: always acquire locks in a consistent global order
- Try-lock with backoff:
pthread_mutex_trylock— if it fails, release all locks and retry
Related
- Processes and Threads — threads share memory, creating the need for sync
- Scheduling — priority inversion from locks
- RTOS Fundamentals — same primitives in embedded systems
- Memory Management — atomic operations need hardware support (memory barriers)