pthreads · race conditions
Three threads increment a shared counter 10 times each. Expected: 30. Actually: usually less — because reading, adding, and writing back isn't atomic. Run it three different ways and watch what survives.
race mode, find an interleaving where the final counter is below 0. Then switch to atomic and verify it always reaches 0. Bonus: use mutex mode and find the longest stall while one thread waits.Source
// pthreads-intro · simplified
int count = 0;
pthread_mutex_t m;
void *worker(void *arg) {
for (int i = 0; i < ITERS; i++) {
// RACE: count = count + 1; // load + inc + store, NOT atomic
// ATOMIC: __atomic_fetch_add(&count, 1, __ATOMIC_SEQ_CST);
// MUTEX: pthread_mutex_lock(&m); count++; pthread_mutex_unlock(&m);
}
return NULL;
}What's really happening
Every thread runs the same C statement: count = count + 1. The CPU breaks that into three real operations — load the current value into a register, increment the register, store the register back to memory.
In race mode, nothing stops two threads from loading the same value, both incrementing to the same new value, and both writing it back. One of those writes is lost. Multiply by hundreds of iterations and the final count is meaningfully wrong — silently, with no error.
atomic uses a hardware-supported single-instruction read-modify-write (lock xadd on x86). The CPU guarantees that no other thread can see or touch the cache line mid-operation. No locks, no waiting — just hardware-level mutual exclusion on a single word.
mutex wraps the critical section in pthread_mutex_lock / pthread_mutex_unlock. The kernel parks any thread that can't get the lock — safe, but with context-switch cost. For one-word counters atomics win; for larger critical sections, mutexes are the right call.