Code of the Day
AdvancedConcurrency with pthreads

Mutexes and race conditions

Protect shared data in C with POSIX mutexes — initialising, locking, and unlocking pthread_mutex_t to eliminate data races.

CAdvanced11 min read
By the end of this lesson you will be able to:
  • 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 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 (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; with atomic_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

  1. Always unlock what you lock. A thread that exits or calls return without unlocking leaves the mutex locked permanently — every other thread blocks forever.
  2. Lock as briefly as possible. The critical section should be the minimum amount of work that must be serialised. Move computations outside the lock.
  3. Do not lock twice. POSIX mutexes are not recursive by default — a thread that calls pthread_mutex_lock while already holding the same mutex deadlocks itself. Use PTHREAD_MUTEX_RECURSIVE if 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.

Finished reading? Mark it complete to track your progress.

On this page