Linux System Programming · intermediate · ~12 min
Recognise a race condition in concurrent C code, and predict the conditions under which it will trigger.
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.
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:
counter.Because the threads are not coordinated, these steps can interleave:
0.1.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.
Sanitisers catch this quickly. Build with ThreadSanitizer (TSan):
gcc -fsanitize=thread
Always test concurrent code with TSan.
#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;
}
++ 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.volatile to "fix" a race. volatile controls compiler caching of a value; it does nothing for atomicity. It will not make a race safe.counter++ can interleave.-fsanitize=thread (TSan).