5.3.14Build Systems & Toolchain

Thread Sanitizer (TSan)

2,312 words11 min readdifficulty · medium

WHY does TSan exist?

WHAT problem class: undefined behavior from concurrent unsynchronized access (torn reads/writes, lost updates, visibility bugs). In C/C++ a data race is straight-up undefined behavior; the compiler may do anything.


The core theory: happens-before


HOW does TSan actually detect this? (derivation from scratch)

We want, for every memory location, to answer: "are these two accesses ordered?" We derive the data structure step by step.

Step 1 — give each thread a clock

Each thread tt has a counter that ticks on each synchronization event. Call it CtC_t.

Step 2 — vector clocks (the heart)

A single counter can't track what one thread knows about others. So thread tt holds a vector clock VtV_t: an array with one entry per thread.

Vt[u]=the latest event index of thread u that t is guaranteed to seeV_t[u] = \text{the latest event index of thread } u \text{ that } t \text{ is guaranteed to see}

Why this step? VtV_t encodes "everything that happens-before now in thread tt." If thread AA's current knowledge of BB already covers BB's write, then that write happened-before — no race.

Rules (derived from the happens-before axioms):

  • Local tick: on its own sync event, Vt[t]+=1V_t[t] \mathrel{+}= 1.
  • Release (unlock LL): copy VtV_t into the lock's clock, VLVLVtV_L \leftarrow V_L \sqcup V_t (componentwise max).
  • Acquire (lock LL): absorb the lock's knowledge, VtVtVLV_t \leftarrow V_t \sqcup V_L.

This is exactly axiom 2 (release/acquire) made mechanical: max merges the histories.

Step 3 — shadow memory per location

For each application byte, TSan stores shadow cells describing the last few accesses: which thread, the thread's clock value at that time, and read/write flag.

Step 4 — the race check

When thread tt accesses location xx, look at a previously recorded access by thread uu (clock kk):

race iff (at least one is a write)    (Vt[u]<k)\text{race iff } (\text{at least one is a write}) \;\wedge\; \big(V_t[u] < k\big)

Figure — Thread Sanitizer (TSan)
Recall Feynman: explain to a 12-year-old

Imagine two kids editing the same Google Doc. If they take turns (one finishes, then tells the other "your turn"), there's never a mess. But if both type into the same line at the same time without telling each other, the words get scrambled — and it only scrambles sometimes, so you can't catch it by re-reading. TSan is a hall monitor who writes down a little timestamp every time a kid writes, and also notes whenever a kid actually passes a note saying "your turn." If kid B edits a line that kid A wrote but A never passed B a note about it, the monitor blows the whistle: "You two might collide!" — even on a day they happened to not collide.


Using TSan (the practical 20%)

# Compile AND link with the flag (it changes the runtime).
clang -fsanitize=thread -g -O1 race.c -o race
./race          # prints a report if a race occurs on this run
  • Works with clang/gcc. Add -g for line numbers, -O1 keeps it fast & readable.
  • Runtime cost: ~5–15× slower, ~5–10× more memory. Run it on tests/CI, not prod.
  • Detects: data races, deadlocks (lock-order inversions), some misuse of pthread/std::mutex, use-after-free in destructors.
  • TSAN_OPTIONS=halt_on_error=1 to stop at first race; suppression files to silence known-benign ones.

Common mistakes (steel-manned)


Forecast-then-Verify drill


Flashcards

What three conditions define a data race?
Same location, at least one write, and the accesses are not ordered by happens-before (concurrent).
Why can two concurrent reads never be a data race?
Neither changes the value, so the order is irrelevant — the result is identical regardless of interleaving.
What is the happens-before relation?
A partial order from (1) program order within a thread, (2) release→acquire synchronization on the same object, and (3) transitivity.
What data structure does TSan use to track ordering?
A vector clock per thread (one counter per thread), merged via componentwise max on synchronization events.
TSan's race check for thread t seeing thread u's prior access at u-time k?
Race if (one is a write) AND V_t[u] < k, i.e. u's access isn't covered by what t knows about u.
Why must -fsanitize=thread be on both compile and link?
Compile instruments memory/sync accesses; link substitutes the special TSan runtime. Both are required.
Why does volatile NOT fix a data race?
It prevents compiler caching but gives no atomicity or happens-before ordering; TSan still sees concurrent unsynchronized access.
Can you combine TSan and ASan in one binary?
No — their runtimes/shadow memory conflict; build separate binaries.
Does a clean TSan run prove the program is race-free?
No — it only analyzes executed code paths; absence of report ≠ proof of absence.
Roughly what is TSan's runtime overhead?
About 5–15× slower and 5–10× more memory; use in tests/CI, not production.
What synchronization makes the classic counter++ race go away, in HB terms?
A mutex: unlock (release) pushes the clock into the lock, later lock (acquire) absorbs it, so V_B[A] ≥ k — ordered.
Mnemonic for race conditions?
WORN — Write, Order absent, Reached by both, Not atomic.

Connections

  • Data Race vs Race Condition — a data race is a memory concept; a race condition is a logic timing bug; overlapping but distinct.
  • Memory Models — happens-before comes from the C++/Java/C11 memory model.
  • std::atomic and Memory Ordering — acquire/release are the edges TSan tracks.
  • AddressSanitizer (ASan) — sibling sanitizer for memory errors (mutually exclusive binary).
  • Mutexes and Locks — provide the release/acquire pairs.
  • Vector Clocks — the distributed-systems concept TSan reuses.
  • Undefined Behavior in C and C++ — why a data race is so dangerous.

Concept Map

is

requires

requires

requires

makes

motivates

detects

watches

watches

defines

built from

built from

implemented via

uses

Data Race

Undefined Behavior in C/C++

Same memory location

At least one write

Not ordered by happens-before

Non-deterministic bug

Testing unreliable

ThreadSanitizer

Memory accesses

Synchronization events

Happens-before relation

Program order

Release/Acquire

Vector clocks

Hinglish (regional understanding)

Intuition Hinglish mein samjho

Dekho, jab do threads ek hi memory location ko chhoo rahe hote hain aur kam se kam ek thread usme write kar raha hai, aur dono ke beech koi synchronization (jaise lock/unlock) nahi hai — to ye ek data race hai. C/C++ me ye undefined behaviour hai, matlab program kuch bhi kar sakta hai. Problem ye hai ki ye bug random hota hai: 10 lakh baar test pass ho jaata hai aur ek baar production me crash. Isliye normal testing kaafi nahi.

TSan (ThreadSanitizer) iska solution hai. Ye sirf ye nahi dekhta ki is run me race hua ki nahi — ye dekhta ki race ho sakta tha ya nahi, by tracking happens-before relationship. Har thread ke paas ek vector clock hota hai (ek choti si array, har thread ka counter). Jab koi thread unlock karta hai (release), apna clock lock me daal deta hai; jab dusra thread lock karta hai (acquire), wo knowledge utha leta hai. Is tarah ek ordering ban jaati hai. Agar do accesses ke beech aisi koi ordering nahi mili aur ek write hai — TSan whistle bajata: "Race!"

Use karna simple hai: clang -fsanitize=thread -g -O1 se compile aur link karo (dono jagah flag chahiye). Run karo, report aa jaayegi line number ke saath. Yaad rakho: volatile se race fix nahi hota — uske liye std::atomic ya mutex use karo. Aur ASan ke saath ek hi binary me TSan nahi chalta, alag build banao. Overhead 5-15x hota hai, isliye CI/tests me chalao, production me nahi.

Bottom line: TSan tumhare concurrency bugs ko catch karta hai pehle hi, lekin "no report" ka matlab "bilkul safe" nahi — sirf executed code paths check hote hain. Mnemonic yaad rakho: WORN — Write, Order absent, Reached by both, Not atomic = race.

Go deeper — visual, from zero

Test yourself — Build Systems & Toolchain

Connections