A race condition occurs when a result depends on the relative order of operations on two or more threads. That dependence is not necessarily a bug: if two threads add items to a queue, either order may be acceptable. A problematic race condition arises when one ordering breaks an invariant. A data race is a specific kind of race condition involving concurrent modification of an object; in C++, it causes undefined behavior. section 3.1.1
A partially deleted list node
Deleting a node from a doubly linked list changes more than one link. If another thread traverses the list after only one link has changed, it sees a broken invariant: it may skip the node or corrupt the structure during another deletion. Protecting each node’s pointers separately is insufficient. The protection must cover the relevant nodes for the entire deletion; a single mutex for the list is one solution. section 3.1 section 3.2.3
Even individually protected operations can form an unsafe sequence. On a shared stack, empty() can report that an element exists, but another thread can pop it before top() runs. Likewise, two threads can both read the same top element before either pops. The problem lies in the interface’s separation of checking, retrieving, and removing—not merely in the choice of locking mechanism. section 3.2.3
Table 3.1. A possible ordering of operations on a stack from two threads (view table figure)
| Thread A |
Thread B |
||
|---|---|---|---|
| if(!s.empty()) | |||
| if(!s.empty()) | |||
| int const value=s.top(); | |||
| int const value=s.top(); | |||
| s.pop(); | |||
| do_something(value); | s.pop(); | ||
| do_something(value); | |||
Follow the two threads’ interleaved stack calls: each passes empty() and reads top() before their pop() calls complete. section 3.2.3
Choose protection around the operation whose result must remain valid:
- Use a mutex to hide intermediate states from other threads, and design interfaces so callers need not rely on a check that can become stale before the next call. section 3.1.2 section 3.2.3
- For one-time initialization, avoid a double-checked locking read outside the mutex: it is not synchronized with the write. std::once_flag and std::call_once provide synchronized initialization. section 3.3.1
- For a lock-free stack push, prepare the new node before publishing it. Use atomic compare/exchange to update the head only if it still has the value previously read; otherwise retry. section 7.2.1
Race conditions can be intermittent: identical inputs may succeed or fail under different thread schedules. Test the smallest concurrent operation likely to expose the problem, and compare with a single-threaded run before concluding that a failure is concurrency-related. section 11.2.2



















