7 routes42 sections324 units

Unit 7.7 · about 4 h

48 / 324

Threads, 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

  1. Threads Basics
    AlgoMaster C++

    What std::thread Actually Is; Four Ways to Construct a Thread; Passing Arguments: By Value, By Reference, By Move; A Word on Destroying a Live Thread

    Depth · Skip overhead estimates and timed-task examples. Correct the lesson's shorthand: a completed but unjoined std::thread is still joinable, and its destructor still terminates the process.

  2. Atomic Operations
    AlgoMaster C++

    Why a Plain int Isn't Safe; Enter std::atomic<T>; Core Operations; Atomic vs Mutex: When to Use Which

    Depth · 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.

  3. Memory ordering
    cppreference

    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.

  4. Avoid data races
    C++ 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_add for 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.
  • join waits 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

With g++ 15.2 -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

  • volatile is 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 join or 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

  1. Two threads use counter.store(counter.load() + 1). Is that equivalent to fetch_add(1)?

  2. When does an acquire load publish the writer's earlier payload writes to the reader?

  3. The worker has returned, but its std::thread has not been joined or detached. Can you destroy that thread object?

  4. Why not use relaxed publication for a non-atomic payload?

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

Resources for this unit

  • AlgoMaster C++
    AlgoMasterprimarycourse
    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
    Standard C++ Foundation communityrequiredreference
    Use the Philosophy, Interfaces, Resource management, Expressions, Containers and concurrency rule groups to review ownership and safety decisions in the native component.
  • cppreference
    cppreference communitysupportreference
    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
    LLVMrequiredimplementation
    Compile seeded memory defects with AddressSanitizer and UndefinedBehaviorSanitizer, preserve the reports, repair the root cause and keep regression tests.
  • pybind11 documentation
    pybind11 projectrequiredimplementation
    Follow First steps and NumPy support to expose one small native function with explicit shape, dtype, stride, ownership, GIL and exception behavior.
  • CMake tutorial
    CMakesupportimplementation
    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
    Sergey Slotinsupportapplication
    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
    Linux man-pages projectsupportreference
    Read DESCRIPTION, Dynamic string tokens and LD_DEBUG to distinguish link-time symbols from runtime shared-library discovery on Linux.
  • Memory Layout Transformations
    Intelsupportreference
    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
    GNU Projectsupportreference
    Use section 8.2 for frame numbers, bt, bt -full and optimized-out arguments when inspecting a native failure.

Quick find

Search Michi

Type to search every route, section, unit, topic and resource.

Enter opens the first result Esc closes

Reference

Keyboard shortcuts

n
Continue to the first unit not passed
j / k
Next / previous unit
1 2 3
Toggle learned, practised, passed
/
Open search
?
Show this help
Esc
Close open panels