136 / 163 · C11 · 8 min
Common Concurrency Problems
This chapter analyzes recurring defect patterns in concurrent software, highlighting the distinction between deadlocks and non-deadlock issues, the latter mainly involving failed atomicity assumptions and reversed execution orders. Synchronization primitives can effectively mitigate these risks and improve the reliability of multithreaded code.
In this lesson
Empirical Study of Defect Patterns
Reviews of mature projects such as databases, servers and office suites indicate that the vast majority of concurrency defects are not deadlocks but arise from mistaken expectations that code fragments are indivisible, or from missing enforcement of event sequencing across threads. Recognizing these high-frequency patterns helps developers avoid issues already at the writing stage.
Breakdown of Atomicity Assumptions
Atomicity breaks down when a programmer believes a group of memory operations must finish consecutively, yet the scheduler lets another thread intervene. A typical case is testing a pointer for validity then immediately dereferencing it, only to have it cleared in between. Wrapping the test and the use inside the same mutex-protected critical section restores the intended indivisibility.
Unexpected Reversal of Execution Order
Certain operations must precede others, for instance an object must be fully constructed before it is read. If the creating thread has not yet returned a handle when the using thread starts accessing it, a null-pointer or undefined-behavior crash follows. Introducing a condition variable together with a state flag lets the latter explicitly wait for the former to finish, thereby locking in the correct timing.
Formation and Breaking of Circular Waits
A circular wait that cannot progress forms when several threads each hold one lock and request another that is already held by a peer. The most direct prevention is a global convention on lock-acquisition order so that a cycle cannot arise at all; timeouts that abandon the attempt or runtime detection can additionally restore the system.
Pitfalls
- Mistakenly treating a single if-statement as already atomic and omitting the lock
- Accessing an object returned by thread creation immediately, without waiting for an initialization signal
- Acquiring multiple locks in opposite orders along different code paths, planting a deadlock seed
Run an example
Minimum C11 · complete program · Download .c
#include <stdio.h>
#include <pthread.h>
static pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
static int ready = 0;
static void *producer(void *arg)
{ (void)arg;
pthread_mutex_lock(&mtx);
ready = 1;
pthread_mutex_unlock(&mtx);
return NULL;
}
static void *consumer(void *arg)
{ (void)arg;
pthread_mutex_lock(&mtx);
if (ready) {
/* safe access under lock */
}
pthread_mutex_unlock(&mtx);
return NULL;
}
int main(void)
{
pthread_t tprod, tcons;
pthread_create(&tprod, NULL, producer, NULL);
pthread_create(&tcons, NULL, consumer, NULL);
pthread_join(tprod, NULL);
pthread_join(tcons, NULL);
printf("Concurrency demo finished successfully.\n");
return 0;
}
Compile locally
gcc -std=c11 -Wall -Wextra -Wpedantic -Werror -pthread ostep-32-concurrency-bugs.c -o example && ./exampleExpected result
Concurrency demo finished successfully.
CHECK YOUR UNDERSTANDING
Close the answer. Explain it.
One thread checks that a shared pointer is non-null and immediately prints its contents, while another thread may set it to null at the same moment. Which class of defect is this, and how should locks be used to repair it?
Show a reference answer
This is an atomicity violation. The repair is to enclose the entire “check-then-print” fragment inside the same mutex so that the pointer cannot be nulled in between.
Check the sources
Drafts and official chapters change. The version mark is only the example’s minimum.