Mutexes and race conditions
Protect shared data in C with POSIX mutexes — initialising, locking, and unlocking pthread_mutex_t to eliminate data races.
- Demonstrate a data race and its non-deterministic outcome
- Initialise and destroy a pthread_mutex_t
- Use pthread_mutex_lock and pthread_mutex_unlock to protect a critical section
- Explain what a critical section is
A data race occurs when two threads access shared memory concurrently and at least one access is a write, without synchronisation. The result is undefined behaviour — the program may produce wrong results, crash, or seem to work until a different machine or compiler reveals the bug.
Demonstrating a race condition
#include <stdio.h>
#include <pthread.h>
static int counter = 0;
void *increment(void *arg) {
(void)arg;
for (int i = 0; i < 1000000; i++) {
counter++; /* data race -- not atomic */
}
return NULL;
}
int main(void) {
pthread_t t1, t2;
pthread_create(&t1, NULL, increment, NULL);
pthread_create(&t2, NULL, increment, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("counter = %d (expected 2000000)\n", counter);
return 0;
}Run this several times. The result varies: 1,127,453 one run, 1,843,201 another. The counter++ operation is three machine instructions (load, add, store), and the OS can interrupt a thread between any two of them.
The mutex
A mutex (mutual exclusion lock) ensures only one thread executes a critical section at a time. The other threads block at pthread_mutex_lock until the lock is released.
#include <pthread.h>
pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER; /* static initialisation */Or dynamic initialisation:
pthread_mutex_t mtx;
pthread_mutex_init(&mtx, NULL); /* NULL = default attributes */
/* ... use ... */
pthread_mutex_destroy(&mtx); /* call when done */Protecting the counter
#include <stdio.h>
#include <pthread.h>
static int counter = 0;
static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
void *increment(void *arg) {
(void)arg;
for (int i = 0; i < 1000000; i++) {
pthread_mutex_lock(&mtx);
counter++; /* critical section */
pthread_mutex_unlock(&mtx);
}
return NULL;
}
int main(void) {
pthread_t t1, t2;
pthread_create(&t1, NULL, increment, NULL);
pthread_create(&t2, NULL, increment, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_mutex_destroy(&mtx);
printf("counter = %d (expected 2000000)\n", counter); /* always 2000000 */
return 0;
}Now the output is always 2,000,000. The mutex ensures only one thread executes counter++ at a time.
Performance trade-offs
The mutex version is correct but slower — each iteration pays the overhead of lock and unlock. For a tight loop doing one increment, this overhead dominates.
Alternatives for simple counters:
__sync_fetch_and_add(&counter, 1)— GCC atomic builtin._Atomic int counter;withatomic_fetch_add(C11<stdatomic.h>).
Atomic operations avoid the lock overhead for simple read-modify-write operations. Use them for counters and flags; use mutexes for protecting larger critical sections.
Rules for mutex use
- Always unlock what you lock. A thread that exits or calls
returnwithout unlocking leaves the mutex locked permanently — every other thread blocks forever. - Lock as briefly as possible. The critical section should be the minimum amount of work that must be serialised. Move computations outside the lock.
- Do not lock twice. POSIX mutexes are not recursive by default — a thread that calls
pthread_mutex_lockwhile already holding the same mutex deadlocks itself. UsePTHREAD_MUTEX_RECURSIVEif you need recursive locking (and consider redesigning instead).
Detecting races at runtime. Compile with -fsanitize=thread (ThreadSanitizer) to detect races. It reports the exact accesses that raced, which threads were involved, and where. Run your test suite under TSan — it is the most effective way to find races before they cause production bugs.
Where to go next
Next: condition variables — the synchronisation primitive that lets threads wait for a condition to become true without busy-looping.