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::mutexpatternsstd::atomicbasics- ThreadSanitizer
- Ten common concurrency bugs (sketches)
Table of contents
- What is a data race?
- Synchronizing with mutex
- Atomic variables
- ThreadSanitizer
- Ten common bugs
- 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)
- Unsynchronized shared counter →
atomicormutex - Concurrent
vectormutation → protect all writers (and readers if writers exist) - False sharing → separate hot atomics to different cache lines (
alignas(64)patterns where justified) - Broken double-checked locking →
std::call_onceor static locals (since C++11) - Condition variable without predicate loop → handle spurious wakeups with
wait(lock, pred) - Reading shared state while another thread writes without synchronization
- Concurrent
unique_ptrreassignment without synchronization - Iterator invalidation across threads (one thread mutates container while another iterates)
- Non-thread-safe singleton patterns →
call_once/ Meyers singleton - 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
| Scenario | Tool |
|---|---|
| Simple counters/flags | atomic |
| Multi-field invariants | mutex |
| Read-mostly maps | shared_mutex (careful) |
| One-time init | std::call_once |
Rules
- No unsynchronized data races on non-atomic objects.
- Prefer
scoped_lockfor multiple mutexes. - TSan on threaded test suites.
- Minimize critical sections; do not release locks mid-iteration if it invalidates iterators.
- Document lock ordering for reviewers.
Related posts (internal)
- 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.