C Mastery / Race Conditions and Data Races
Part 10 — Concurrency

Race Conditions and Data Races

This chapter distinguishes race conditions (logic bugs from timing) from data races (concurrent access to a non-atomic object, which is undefined behavior).

Why This Matters

"Race" is used loosely, but in C a *data race* has a precise meaning and is UB. A race condition can exist even with no data race. Knowing the difference determines whether your code is merely buggy or fundamentally undefined.

Prerequisites

Core Concept

Data race

Two operations conflict if they access the same memory location and at least one is a write. A data race occurs when two conflicting operations on a non-atomic object are executed by different threads without a happens-before relationship.

Data races are undefined behavior. VERIFIED

Race condition

A race condition is a logic flaw where the program's outcome depends on the timing/order of events. It can occur even when all accesses are properly synchronized (e.g., two threads each check a flag and act — the *interleaving* of check-then-act is the bug).

Examples

Data race (UB)

#include <threads.h>

static int shared = 0;

int writer(void *arg)
{
    (void)arg;
    shared = 1;   /* write */
    return 0;
}

int reader(void *arg)
{
    (void)arg;
    int v = shared;   /* read, no synchronization -> data race */
    return v;
}

Race condition (check-then-act)

static atomic_int flag = 0;

int init_once(void *arg)
{
    (void)arg;
    if (atomic_load(&flag) == 0) {      /* check */
        /* ... initialize ... */
        atomic_store(&flag, 1);         /* act */
    }
    return 0;
}

Two threads can both see flag == 0 before either stores, running the initialization twice — a race condition, even though each atomic access is well-defined.

How It Works

The C memory model defines a happens-before relation. Synchronization (mutexes, atomics with proper order) establishes it. Without it, conflicting accesses are a data race and UB. Race conditions are higher-level timing bugs, independent of the memory model.

Variations

Synchronized race condition

A race condition can persist even with correct locking if the *algorithm* is not atomic across multiple operations (check-then-act, read-modify-write across separate statements).

Benign races

Some code uses volatile or relaxed atomics for flags where the logic is eventually-consistent; this is still a data race if the object is non-atomic.

Common Mistakes

race.

reasoning about surrounding non-atomic data.

Undefined Behavior

Portability

conditions are language-independent logic bugs.

Under the Hood

ThreadSanitizer (c.debug.tsan) detects data races at run time by tracking accesses and the happens-before graph. Race conditions require reasoning about interleavings.

Practical Usage

single atomic compare-exchange).

Exercises

1. Write a data race and detect it with TSan. 2. Write a check-then-act race condition (no data race) and explain it. 3. Fix both examples: one with a mutex, one with an atomic compare-exchange.

Deep Challenge

Explain precisely why volatile does not prevent a data race, citing the C memory model. Then show the correct atomic or mutex-based fix.

References

Verification

verified.`

Progress

Concept checkboxes

Mastery levels

ConceptCurrent level (0–8)Target level
c.conc.race06
c.conc.data-race07