C++ Multithreading Crashes: Data Races, mutex, atomic, and ThreadSanitizer

C++ Multithreading Crashes: Data Races, mutex, atomic, and ThreadSanitizer

이 글의 핵심

Practical guide to data races in C++: when to use atomics vs mutexes, avoiding iterator invalidation across threads, TSan workflow, and common concurrency pitfalls.

Data races are UB (undefined behavior). Iterator misuse across threads overlaps with iterator invalidation.

Introduction: “Multithreaded code crashes sometimes”

“Single-threaded version was fine”

The most common serious bug in concurrent C++ is a data race: unsynchronized access to shared mutable state.

int counter = 0;

void worker() {
    for (int i = 0; i < 1'000'000; ++i) {
        ++counter;  // data race
    }
}

This article covers:

  • Data races vs informal “race conditions”
  • std::mutex patterns
  • std::atomic basics
  • ThreadSanitizer
  • Ten common concurrency bugs (sketches)

Table of contents

  1. What is a data race?
  2. Synchronizing with mutex
  3. Atomic variables
  4. ThreadSanitizer
  5. Ten common bugs
  6. Summary

1. What is a data race?

A data race (in the standard sense) requires conflicting accesses without synchronization. It is undefined behavior.

Typical outcomes: torn reads/writes, crashes, or “lucky” passes.


2. mutex

#include <mutex>

int counter = 0;
std::mutex mtx;

void worker() {
    for (int i = 0; i < 1'000'000; ++i) {
        std::lock_guard<std::mutex> lock(mtx);
        ++counter;
    }
}

Deadlock avoidance

Always acquire multiple mutexes in a consistent global order, or use std::scoped_lock (C++17) / std::lock to lock several mutexes atomically.


3. Atomics

#include <atomic>

std::atomic<int> counter{0};

void worker() {
    for (int i = 0; i < 1'000'000; ++i) {
        ++counter;  // atomic RMW
    }
}

Rule of thumb: one simple counter or flag → often atomic. Multiple fields that must move together → mutex.


4. ThreadSanitizer

g++ -g -fsanitize=thread -std=c++17 -o myapp main.cpp
./myapp

TSan reports racing lines with stacks—fix the synchronization at those sites.


5. Ten common bugs (overview)

  1. Unsynchronized shared counteratomic or mutex
  2. Concurrent vector mutation → protect all writers (and readers if writers exist)
  3. False sharing → separate hot atomics to different cache lines (alignas(64) patterns where justified)
  4. Broken double-checked lockingstd::call_once or static locals (since C++11)
  5. Condition variable without predicate loop → handle spurious wakeups with wait(lock, pred)
  6. Reading shared state while another thread writes without synchronization
  7. Concurrent unique_ptr reassignment without synchronization
  8. Iterator invalidation across threads (one thread mutates container while another iterates)
  9. Non-thread-safe singleton patterns → call_once / Meyers singleton
  10. Too-small critical sections split across logically atomic updates

(See code blocks in the Korean article for concrete snippets; principles above map 1:1.)


Patterns: thread pool sketch & shared_mutex

A work queue with mutex + condition_variable is the standard producer/consumer pattern: hold the lock only to take/push tasks; execute work outside the lock.

std::shared_mutex: many concurrent readers or one writer—ideal for read-heavy caches if invariants are simple.


Summary

Checklist

  • Every shared mutable object has a clear synchronization policy?
  • Reads synchronized when writes may occur?
  • Lock ordering documented to avoid deadlock?
  • Condition variables use predicates?
  • TSan runs on CI for threaded tests?

Tool choice

ScenarioTool
Simple counters/flagsatomic
Multi-field invariantsmutex
Read-mostly mapsshared_mutex (careful)
One-time initstd::call_once

Rules

  1. No unsynchronized data races on non-atomic objects.
  2. Prefer scoped_lock for multiple mutexes.
  3. TSan on threaded test suites.
  4. Minimize critical sections; do not release locks mid-iteration if it invalidates iterators.
  5. Document lock ordering for reviewers.

  • Data races: mutex & atomic
  • Multithreading overview

Keywords

multithreaded crash, data race, mutex, atomic, ThreadSanitizer, TSan, concurrency

Practical tips

  • Run TSan locally on any new concurrent code.
  • Treat intermittent failures as races until proven otherwise.
  • Write down lock order for multi-lock code paths.

Closing

Concurrent bugs are hard to reproduce; ThreadSanitizer turns many races into actionable reports. Pair clear ownership of shared state with mutex/atomic discipline.

Next: Explore lock-free programming only after solid mutex-based designs and measurements justify it.