Thread Sanitizer (TSan)
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 has a counter that ticks on each synchronization event. Call it .
Step 2 — vector clocks (the heart)
A single counter can't track what one thread knows about others. So thread holds a vector clock : an array with one entry per thread.
Why this step? encodes "everything that happens-before now in thread ." If thread 's current knowledge of already covers 's write, then that write happened-before — no race.
Rules (derived from the happens-before axioms):
- Local tick: on its own sync event, .
- Release (unlock ): copy into the lock's clock, (componentwise max).
- Acquire (lock ): absorb the lock's knowledge, .
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 accesses location , look at a previously recorded access by thread (clock ):

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-gfor line numbers,-O1keeps 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=1to 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?
Why can two concurrent reads never be a data race?
What is the happens-before relation?
What data structure does TSan use to track ordering?
TSan's race check for thread t seeing thread u's prior access at u-time k?
Why must -fsanitize=thread be on both compile and link?
Why does volatile NOT fix a data race?
Can you combine TSan and ASan in one binary?
Does a clean TSan run prove the program is race-free?
Roughly what is TSan's runtime overhead?
What synchronization makes the classic counter++ race go away, in HB terms?
Mnemonic for race conditions?
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
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.