Linux System Programming · intermediate · ~12 min

Race conditions

Recognise a race condition in concurrent C code, and predict the conditions under which it will trigger.

Lesson

What is a race condition?

A race condition happens when two or more threads access the same memory, at least one of them writes to it, and they do not coordinate that access.

The outcome then depends on timing: which thread happens to run first. In C, this is undefined behaviour ("UB" — the C standard places no constraints on what the program may do). The compiler is even allowed to optimise the code as if the race could never occur.

A classic example: a shared counter

The simplest demonstration is two threads incrementing the same counter.

int counter = 0;
/* thread A and thread B both run: */
counter++;          /* read counter, add 1, write counter — three steps */

The key insight is that counter++ is not a single action. It is three steps:

  1. Read the current value of counter.
  2. Add 1 to it.
  3. Write the new value back.

Because the threads are not coordinated, these steps can interleave:

  • Both threads read 0.
  • Both compute 1.
  • Both write 1.

One increment is lost. Run two threads that each increment 1,000,000 times, and the final value will be noticeably smaller than 2,000,000.

Detecting races

Sanitisers catch this quickly. Build with ThreadSanitizer (TSan):

gcc -fsanitize=thread

Always test concurrent code with TSan.

Code examples

#include <pthread.h>
#include <stdio.h>

static long counter = 0;
static void *bump(void *_) {
    (void)_;
    for (long i = 0; i < 1000000; i++) counter++;
    return NULL;
}

int main(void) {
    pthread_t a, b;
    pthread_create(&a, NULL, bump, NULL);
    pthread_create(&b, NULL, bump, NULL);
    pthread_join(a, NULL); pthread_join(b, NULL);
    printf("counter = %ld (expected 2000000)\n", counter);
    return 0;
}

Common mistakes

  • Assuming ++ is atomic on x86. It is not. Even on x86, counter++ is a read-modify-write sequence, and the read and the write can be split apart by another thread.
  • Using volatile to "fix" a race. volatile controls compiler caching of a value; it does nothing for atomicity. It will not make a race safe.

Summary

  • A race condition: two or more threads access shared memory, at least one writes, and there is no coordination.
  • The result is undefined behaviour, because the steps of an operation like counter++ can interleave.
  • Detect races by building with -fsanitize=thread (TSan).
  • The fix is a mutex, covered in the next lesson.

Practice with these exercises