Before we start, let us re-earn the vocabulary — including the one piece of notation the "why" answers use.
Recall The three words every answer leans on
Happens-before (a→b): b is guaranteed to see everything a did. Concurrent: neither a→b nor b→a — the order is genuinely undecided. Data race: two accesses to the same location, concurrent, with at least one write.
The picture makes the rule concrete: the red arrow is the note passed (the unlock→lock edge). If it exists, B's clock knows A's write and VB[A]≥k. If it is missing, B never learned, VB[A]<k, and the whistle blows.
Two threads that only read the same global variable can data-race.
False — a race needs at least one write. Two reads leave the value unchanged, so the order between them cannot affect any outcome; nothing to detect.
A program that produced the correct answer on this run is proven race-free.
False — TSan reasons about possible orderings via happens-before, not the one ordering that happened. A correct run can still contain a race that only corrupts data on a rarer interleaving.
volatile int is enough to make a shared counter thread-safe.
False — volatile only stops the compiler from caching the value; it creates no happens-before edge and no atomicity, so two concurrent writes are still a data race. Use std::atomic or a mutex.
A data race in C/C++ is undefined behavior, not merely "a wrong number."
True — the standard makes any data race UB, so the compiler may assume it never happens; the result is not "some value," it is anything at all.
If TSan prints no report, the program is guaranteed race-free.
False — TSan only observes code paths actually executed on that run, and cannot model custom lock-free tricks it doesn't understand. It finds races; it does not prove their absence.
Compiling with -fsanitize=thread but linking without it still gives full detection.
False — TSan instruments accesses at compile time and swaps in a special runtime at link time. Miss either stage and detection is broken; pass the flag to both.
Building one binary with both -fsanitize=thread and -fsanitize=address doubles your coverage.
False — the two runtimes and shadow-memory schemes conflict and are incompatible in a single binary. Build separate binaries for ASan and TSan.
A deadlock is a kind of data race.
False — a deadlock is threads stuck waiting on each other's locks; no unsynchronized memory access is involved. TSan happens to also detect lock-order inversions, but they are a different class of bug from data races.
Adding a printf between two accesses removes a real data race.
False — the printf may perturb timing so the bad interleaving stops appearing, but it adds no happens-before edge between the two threads' accesses. The race is still there and still UB.
If thread A writes and thread B reads after a pthread_join(A), that is a race.
False — join is a synchronization event that creates a happens-before edge: everything A did happens-before B's code after the join, so B is guaranteed to see the write.
"I protected the writes with a mutex, so the unlocked reads elsewhere are safe."
A race needs synchronization on both conflicting accesses. If one thread reads the same location without holding the lock, that read is concurrent with a locked write — still a data race.
"I used two different mutexes to guard the same variable in two threads."
Release/acquire only builds a happens-before edge through the same lock object. Two different locks never synchronize with each other, so the accesses remain concurrent — a race.
"Each thread locks, increments, unlocks — but they lock in opposite orders across two mutexes."
The increments are race-free, but the opposite ordering can deadlock — see the cycle drawn below. Fix by making every thread take locks in one global order (always L1 then L2).
Trace the inversion step by step: (1) thread A locks L1, (2) thread B locks L2, (3) A now blocks trying to take L2 which B holds, (4) B blocks trying to take L1 which A holds. Each waits for a lock the other owns — a cycle in the "waits-for" arrows (the red loop) — and neither ever moves.
"My counter is int and I only ever do counter++, which is a single instruction."
counter++ is a read-modify-write: load, add, store. Without atomicity two threads can interleave and lose an update — the classic race in the parent note's example.
"I marked the shared flag const so it can't be a problem."
const is a compile-time promise about this view of the object; it says nothing about other threads writing the underlying memory. If any thread writes it, concurrent access is still a race.
"I added a sleep(1) in one thread so they can't run at the same time."
Sleeping only changes probability, not ordering guarantees — nothing in the memory model orders the accesses. On a slow or loaded machine they can still overlap; the race is unfixed.
"The variable is only written once, at startup, then read by everyone — safe."
Only safe if a happens-before edge (e.g. thread creation, which orders the parent's earlier writes before the child) separates that write from all reads. If the write can happen after some thread already started reading, it is still a race.
Why does TSan catch a race even on a run where no corruption occurred?
Because it does not wait for the bad interleaving to happen; it uses vector clocks to check whether two accesses are ordered by happens-before. If they are provably concurrent and one is a write, that is a race regardless of what this run produced.
Why is "at least one write" part of the definition and not "at least one access"?
If both accesses are reads, neither changes the value, so the outcome is identical in either order — there is nothing undefined. A write makes the final value depend on who went last.
Why does an unlock/lock pair remove the race but volatile does not?
Concretely: A does unlock(m), which copies A's clock (with A's write recorded at counter k) into the lock object; B later does lock(m), which copies that clock back out, so now VB[A]≥k — B is ordered after A's write. volatile performs neither copy, so B's clock never learns of A's write and VB[A]<k stays true.
Why does the opposite-lock-order case deadlock instead of just running slowly?
Walk it: A holds L1 and needs L2; B holds L2 and needs L1. Neither will release what it holds until it gets the other lock, so both wait forever — a true cycle, not a slowdown. There is no interleaving that breaks it, which is why TSan can flag the inversion up front.
Why must TSan see synchronization events, not just memory accesses?
Accesses alone can't tell it whether two touches were ordered. It needs the release/acquire events to build the happens-before graph; without them every pair would look concurrent and it would drown in false positives.
Why is TSan run in CI rather than in production?
It costs roughly 5–15× CPU and 5–10× memory because it instruments every access and tracks shadow state. That overhead is fine for tests but unacceptable for shipping.
Why can a race be undefined behavior instead of just a "sometimes-wrong value"?
The memory model declares racy programs to have no defined meaning, so the compiler is free to optimize as if races never occur — reordering, tearing, or deleting code. The observable result is unbounded.
Thread A writes a shared value, then creates thread B which reads it. Race?
No — thread creation is a synchronization event: everything A did before spawning B happens-before B's first instruction, so B sees the write.
Thread B reads a shared value, then A (already running) writes it later — no sync between them. Race?
Yes — read-then-write on the same location with at least one write and no ordering is a race. Order of the two operations doesn't rescue it; concurrency does the damage.
A std::atomic variable read and written with memory_order_relaxed by two threads. Race?
No data race — atomic accesses can never be a data race by definition. But relaxed ordering builds no happens-before edge for other variables, so a plain (non-atomic) variable published "through" this relaxed flag can still race, and higher-level logic bugs remain.
A thread stores an atomic flag with memory_order_release, another loads it with memory_order_acquire, then reads a plain variable the first thread wrote. Race on the plain variable?
No — release/acquire on the same atomic creates exactly the happens-before edge a mutex would: the writer's clock (with the plain write at counter k) reaches the reader, so VB[A]≥k. This is the standard lock-free "publish" pattern.
The same publish pattern but the writer uses release and the reader uses relaxed (mismatched). Race on the plain variable?
Yes — the edge only forms when the load is an acquire (or stronger). A relaxed load synchronizes with nothing, so the plain write is not ordered before the read; VB[A]<k and TSan flags it.
Two atomics ordered by memory_order_seq_cst in both threads. Any subtlety over acquire-release?
For these two variables, seq-cst gives you acquire-release plus a single global order all threads agree on. It never introduces a race; it only removes exotic reordering paradoxes that plain acquire-release can allow across multiple atomics.
A std::atomic counter incremented with fetch_add by two threads, no lock. Race?
No — fetch_add is a single atomic read-modify-write, so no update is lost and it is never a data race, even with relaxed ordering. (Ordering choice only affects visibility of other variables around it.)
A single-threaded program that reuses a buffer. Any data race?
No — a data race requires two threads. Within one thread program order fully orders every access; the bug would be a different category (e.g. use-after-free, which ASan handles).
Two threads write the same value to a shared byte concurrently. Race?
Yes — the definition is about whether a write is concurrent, not whether the values differ. Even identical writes to the same unsynchronized location are UB.
Thread A releases a mutex; thread B never acquires that mutex but touches the guarded data. Race?
Yes — the happens-before edge only forms when B acquires the same lock A released. Skipping the acquire leaves B's access concurrent with A's.
A lock-free queue using a custom atomic protocol TSan doesn't model — reported clean. Trustworthy?
Not fully — TSan can miss ordering it doesn't understand and only saw the executed paths. Treat a clean report as evidence, not proof; pair it with thorough tests.