liveBook · Manning

Race condition

As explained in C++ Concurrency in Action, Second Edition

A race condition is a program condition in which behavior depends on the timing or ordering of operations performed by concurrent threads.

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

How C++ Concurrency in Action, Second Edition covers it

How deep it goes
Works through interleaved list and stack operations, then develops mutex and atomic alternatives without detailing the memory model here.
Examples
Uses ticket purchases, doubly linked list deletion, interleaved stack calls, lazy initialization, and lock-free stack push.
What it assumes
Uses C++ container operations while defining thread-order dependence from a ticket-purchase analogy.
What you can do after
Diagnose/debug with it by checking which interleavings violate invariants, then choose protection covering the complete operation.
How it connects
Connects race conditions to data races, invariants, mutexes, lock-free programming, and atomic compare/exchange.

Also explained in

Compare how 20 books explain race condition →

Related concepts

This explainer was generated by Manning's knowledge graph from the text of C++ Concurrency in Action, Second Edition. The section links point to the parts of the book it draws on.