Unit 7.7 · about 4 h
48 / 324Threads, data races, memory ordering and atomics
Native threads can access the same storage without Python's protections. Atomicity, visibility and thread lifetime must be explicit before a compute loop can safely run concurrently.
- explain
You can now
- Identify a data race and distinguish it from a race-free but incorrect sequence of atomic operations.
- Explain how mutexes, atomic read-modify-write and release/acquire publication provide different guarantees.
- Trace thread ownership and the lifetime of captured data through joining and detachment.
Covers
- Threads, data races, memory ordering and atomics at literacy depth
Study, in this order
4 steps
- Threads BasicsAlgoMaster C++
What
std::threadActually Is; Four Ways to Construct a Thread; Passing Arguments: By Value, By Reference, By Move; A Word on Destroying a Live ThreadDepth · Skip overhead estimates and timed-task examples. Correct the lesson's shorthand: a completed but unjoined
std::threadis still joinable, and its destructor still terminates the process. - Atomic OperationsAlgoMaster C++
Why a Plain
intIsn't Safe; Enterstd::atomic<T>; Core Operations; Atomic vs Mutex: When to Use WhichDepth · Use the race example only to identify undefined behavior, not to predict lost updates or instruction counts. Skip CAS algorithms, lock-free reclamation, timing estimates and assembly claims. Atomics can use internal locks; an uncontended mutex need not make a system call.
- Memory orderingcppreference
Constants; Explanation: Relaxed ordering, Release-Acquire ordering, Sequentially-consistent ordering and Relationship with volatile
Depth · Read the C++20 rules and the publication example. Skip consume ordering and the full formal model.
- Avoid data racesC++ Core Guidelines
CP.2: Avoid data races, including concurrent mutation of the same buffer
Focus on
- Conflicting accesses in different threads, at least one non-atomic and with no happens-before ordering, can form a data race. A data race is undefined behavior, not a promise of a particular wrong count.
- Atomic load followed by atomic store is not one atomic increment. Use
fetch_addfor that operation; use a mutex to protect a multi-field invariant. - Relaxed atomic operations preserve atomicity and per-object modification order, not publication of unrelated data. A release store read by an acquire load can publish preceding writes.
joinwaits for completion and establishes synchronization with that completion. Detachment removes the join handle; it does not extend the lifetime of referenced objects.
Reproduce closed-book
-std=c++20 -O0 -g -pthread, start two threads, each performing 1,000 fetch_add(1, memory_order_relaxed) operations on atomic<int>{0}; join both and check the final count is 2,000. Explain why joining makes the final read appropriate even with relaxed increments. Separately initialize int payload = 0 and atomic<bool> ready{false}; one writer sets payload = 42 then stores ready = true with release, and one reader spins on an acquire load until true before reading payload. Check it reads 42, and draw the synchronizes-with and happens-before chain. Keep it a one-shot protocol with one writer. Do not run a relaxed-flag version to infer a result: the unprotected payload access would have undefined behavior and no particular output is guaranteed.Traps
volatileis not inter-thread synchronization and does not turn++into an atomic operation.- Separate atomic fields do not automatically preserve a relationship between those fields.
- Sleep is not a substitute for
joinor publication. Elapsed time establishes no happens-before relationship.
Practice
AlgoMaster Exercise: Atomic Operations
Exercise 2: Count Sales Across Threads; Exercise 4: Keep the Lowest Score with Compare Exchange; Exercise 5: Publish Data with Acquire and Release
For Exercise 5, draw the release-acquire edge that makes the plain payload read race-free.
AlgoMaster Exercise: Joining and Detaching Threads
Exercises 1 to 5, especially Exercise 4: Join in an RAII Destructor During an Exception and Exercise 5: Join a Thread Group During Stack Unwinding
State who owns each thread handle and when its joinability ends.
Self-test
-
Two threads use
counter.store(counter.load() + 1). Is that equivalent tofetch_add(1)?No. The load and store are separate atomic operations, so both threads can read the same old value and overwrite each other's update. This is a logical race without a data race on the atomic itself. -
When does an acquire load publish the writer's earlier payload writes to the reader?
When it reads the value from the corresponding release operation, or an applicable release sequence. Merely choosing acquire is not enough. -
The worker has returned, but its
std::threadhas not been joined or detached. Can you destroy that thread object?Its destructor callsstd::terminatebecause it is still joinable. Completion alone does not clear joinability. -
Why not use relaxed publication for a non-atomic payload?
The relaxed flag does not order the payload write before the reader's access. Without another synchronization edge, the conflicting accesses form a data race.
Completion criterion
Stop when
You reproduce the counter and one-shot publication checks, draw the payload's happens-before chain, and distinguish data races from incorrect but atomic update sequences.
Mark passed, then open 7.8 Only mark this when you can do it without notes.Leads to
- 7.8 Python and native code: dtype, stride, ownership and GIL contracts Reason about native shared state when the GIL is released or absent.
- 6.2 Synchronization primitives and concurrency hazards Relate native synchronization to the route's broader concurrency contracts.
Resources for this unit
- AlgoMaster C++ Use the sections on compilation, pointers and references, memory, RAII and smart pointers, copy and move, STL containers, templates, errors, concurrency and paired exercises. Skip competitive-programming breadth already covered in Algorithms.
- C++ Core Guidelines Use the Philosophy, Interfaces, Resource management, Expressions, Containers and concurrency rule groups to review ownership and safety decisions in the native component.
- cppreference Consult language lifetime, object model, value categories, memory model, atomics, containers and iterator invalidation while reading or modifying code.
Also in this section
- Clang AddressSanitizer and UndefinedBehaviorSanitizer Compile seeded memory defects with AddressSanitizer and UndefinedBehaviorSanitizer, preserve the reports, repair the root cause and keep regression tests.
- pybind11 documentation Follow First steps and NumPy support to expose one small native function with explicit shape, dtype, stride, ownership, GIL and exception behavior.
- CMake tutorial Use the introductory steps for targets, libraries, testing and install rules. Produce one clean debug build and one optimized build without copied one-off commands.
- Algorithmica - AoS and SoA Read the AoS and SoA experiment for a measured case where keeping each record together wins, the counterpart to a single-field scan.
- `ld.so`: dynamic linker/loader Read DESCRIPTION, Dynamic string tokens and
LD_DEBUGto distinguish link-time symbols from runtime shared-library discovery on Linux. - Memory Layout Transformations Read Overview and Topics through AOS Form and SOA Form for field-wise access and locality trade-offs. Skip Intel array-notation syntax and the LIBOR example.
- GDB manual: Backtraces Use section 8.2 for frame numbers,
bt,bt -fulland optimized-out arguments when inspecting a native failure.