5.3.14 · D1Build Systems & Toolchain

Foundations — Thread Sanitizer (TSan)

2,683 words12 min readBack to topic

This page builds every symbol the parent note leans on, starting from "what is a thread" and ending at the $V_t[u] < k$ race check. Nothing is assumed. If you already know a section, the reveal lines at the bottom let you skip-test.


1. Thread — the actor that does the touching

Picture two fingers on the same book. Each finger reads its own line, but sometimes both fingers reach for the same word on the same page. That shared word is shared memory (next section).

Figure — Thread Sanitizer (TSan)

Why the topic needs it: a race requires two actors. One finger alone can never race with itself — it does one thing at a time. The whole subject only exists because more than one thread runs concurrently.


2. Memory & shared location — the thing being touched

Picture the row of boxes as mailboxes on a street. Box number x is "shared" if thread A and thread B both have a key to it.

Why the topic needs it: the parent's definition says "two threads touching the same memory." That "same box" is the location you'll see in the race check. If two threads only touch different boxes, there is never a race. See Memory Models for how boxes and values behave across cores.


3. Access, Read vs Write — the two kinds of touch

Throughout this page we name two particular accesses with the letters and : think of them as two labelled dots, "the touch called " and "the touch called ", each one performed by some thread on some box.

Distinguish this from a higher-level bug: see Data Race vs Race Condition — a data race is the low-level memory-level fault, not the same thing as a logic-level race condition.


4. Event and its index — the numbered heartbeats of a thread

Picture each thread as a ticking stopwatch that only advances when the thread does something worth recording. The reading on 's stopwatch at the moment of an event is that event's index.

Which events tick the clock? To keep the machinery simple and correct, the parent's rules advance a thread's own counter on synchronization events (lock, unlock, create, join) — not on every ordinary read or write. Plain accesses are merely stamped with the current counter value; they do not increment it. That is why counts sync events, not memory touches.

Why the topic needs it: "the latest event index of that can see" (section 7) only makes sense once "event" and "index" are defined. The stamp in section 8 is an event index.


5. Synchronization — the "your turn" note

Two more synchronization pairs matter for a complete picture:

  • Thread create → child's first event: the moment you spawn a thread, everything the parent did before the spawn is visible to the child (an arrow from parent to child).
  • Child's last event → join: when you pthread_join a thread, everything that thread did is visible to you afterwards (an arrow from child to joiner). This is why "write, then join, then read" is safe.
  • Atomic release → atomic acquire: a store to a std::atomic with release ordering, paired with a load that acquires it, is also a synchronization pair — the same release/acquire handshake as a mutex, without a lock.

Picture a talking stick: only the finger holding the stick may touch the shared box. Handing the stick over (unlock → lock, or create, or join, or atomic release → acquire) is the moment one thread's work becomes visible and ordered before the other's.

Why the topic needs it: synchronization is the only thing that creates order between threads. Without it, two accesses float free, unordered — the exact condition for a race. These pairs are what build the happens-before edge (section 7).


6. Concurrent, AND, NOT — "could go either way"

Figure — Thread Sanitizer (TSan)

Why the topic needs it: the whole point of TSan is to catch concurrent-plus-write pairs without the bad interleaving actually happening. "Concurrent" is precisely "not ordered by happens-before."


7. Happens-before () — the arrow of guaranteed order

You already have all three ways an arrow appears:

  1. Program order — same thread, earlier event index: your one finger does event 1 before event 2, so event-1 event-2 (sections 1, 4).
  2. Synchronization — an unlock the matching lock, create child, child join, or atomic release acquire (section 5).
  3. Transitivity — if and , then . Arrows chain.

Why the topic needs it: is the entire theory. TSan does not watch time; it watches these arrows. No arrow between two accesses (one a write) = whistle.


8. Vector clock and the entry — remembering the arrows

A plain counter per thread can say "I have done 5 sync events," but not "how much of thread B's history I am guaranteed to have seen." We need that second thing for the race check. So we upgrade to an array.

Figure — Thread Sanitizer (TSan)
  • = how many sync events my own thread has done (my personal clock).
  • for = how far into thread 's past I am guaranteed to see.

Why the topic needs it: the race check reads exactly one number, , and compares it. Everything above exists to give that number a meaning: "the deepest point in 's history that is ordered after."


9. The stamp and the comparison — reading the whistle

First, the new symbol. When thread last touched box , TSan recorded when — specifically 's own event index at that instant. Call that recorded number (a plain integer, an event index from section 4). Only now can we write the check.

Figure — Thread Sanitizer (TSan)

Why the topic needs it: this single < comparison is the whistle. It converts the abstract "are they ordered?" into one integer test the runtime can do millions of times per second.


How the foundations feed the topic

The map below is not just a table of contents: follow the arrows and you are literally tracing how a stamp becomes a verdict. A write on a shared box creates an access; that access is an event with an index; the index gets remembered in a vector clock via synchronization merges; and the final < test turns two clocks into a yes/no race answer. Each arrow is a "feeds into," so a break anywhere upstream (say, no synchronization) is exactly what produces "concurrent" downstream.

Thread

Shared memory box

Access read or write

Event and its index

Synchronization lock unlock create join atomic

Happens-before arrow

Concurrent unordered

Vector clock V_t

Entry V_t of u

Stamp k the recorded index

Check V_t of u less than k

Race verdict whistle


  • Why an unsynchronized race is not just wrong but undefined: Undefined Behavior in C and C++.
  • The correct tools that create happens-before arrows: Mutexes and Locks and std::atomic and Memory Ordering.
  • The memory-level cousin sanitizer (a different fault class, incompatible in one binary): AddressSanitizer (ASan).
  • The hardware rules behind visibility: Memory Models.

Equipment checklist

Each line: term
meaning — (where it was built / why it matters).
A thread is
one line of execution, a single finger tracing code — (§1; a race needs two of these).
A shared memory box is
one numbered location two or more threads can touch — (§2; the "same memory" in the race definition).
An access is
one thread's single touch of a box, a read or a write — (§3; the things and in the formula).
Why at least one access must be a write
two reads never change the value, so order can't affect the result; only a write makes the outcome depend on who went last — (§3; the "W" of a race).
An event and its index are
any noteworthy step, numbered 1,2,3… in program order; the index is that number — (§4; gives meaning to "latest event of u").
Which events tick a thread's own counter
synchronization events (lock, unlock, create, join); plain reads/writes are only stamped, not counted — (§4; explains why counts syncs).
The synchronization pairs are
unlock→lock, create→child, child→join, atomic release→acquire — (§5; the only sources of cross-thread arrows).
(AND) and NOT mean
both-true, and arrow-absent — (§6; the glue of the formal race line).
"Concurrent" means
unordered — nothing forces one before the other; NOT necessarily simultaneous — (§6; the "O" of a race).
(happens-before) means
access is guaranteed complete and visible before starts — (§7; the whole theory in one arrow).
The three sources of a happens-before arrow are
program order, synchronization pairs, transitivity — (§7; how arrows get built and chained).
The vector clock entry holds
the latest event index of that is guaranteed to have seen — (§8; the one number the check reads).
Initial clock values and new-thread slots are
all slots start at 0; a new thread inherits its creator's clock and adds its own slot — (§8; no undefined starting state).
(merge) does
componentwise max, per slot — (§8; the handshake made arithmetic).
The stamp is
's own event index recorded at its last access to the box — (§9; what the check compares against).
The race check is true when
does NOT see 's recorded access, so they are concurrent (a race if a write is involved) — (§9; the whistle).
means
is ordered after 's access — an arrow exists — no race — (§9; the all-clear).