σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
uantCraft AcademyRoadmapsSkills
Ctrl K
AO
+1.2k Learners
LoginJoin ↗
RoadmapsSkills
AO
+1.2k Learners
Login
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
uantCraft AcademyRoadmapsSkills
Ctrl K
AO
+1.2k Learners
LoginJoin ↗
RoadmapsSkills
AO
+1.2k Learners
Login
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
Lock-free skill›Module 4 · Testing and memory safety›Topic 4.1·~55 minAdvanced

Testing concurrent code: race detectors and model checking

Skill positionModule 4 of 4 · Topic 1 of 3
4.1 Testing concurrent code: race detectors and model checking
—Lab score— / 100Total XP—Streak—

5 min readCode examples: 4

  1. 01A trace and a question
  2. 02Lockset: which lock guards this variable?
  3. 03Happens-before: what could really overlap
  4. 04How big is the space?
  5. 05What you will build

Running every schedule proves a small program, but it costs the number of interleavings. Real tools take two shortcuts. A race detector watches one run and reports the variables that could race in some other schedule. A model checker remembers the states it has seen. For two threads of 10 steps that is 184,756 schedules against 121 states.

01

A trace and a question

A race detector does not run threads. It reads a trace: every read and write of shared data, and every acquire and release of a lock. Our programs become lists of such operations, and interleave turns a program and a schedule into the trace of that run. Two threads that each read and write x and use no lock give the four events below. In this schedule thread 0 finishes before thread 1 starts, so a test would see the right answer. The question for the detector is different: could the two threads have overlapped?

main.pyPython
def interleave(programs, schedule):    position = [0] * len(programs)    trace = []    for tid in schedule:        kind, name = programs[tid][position[tid]]        position[tid] += 1        trace.append((tid, kind, name))    return traceunlocked = [[("read", "x"), ("write", "x")], [("read", "x"), ("write", "x")]]trace = interleave(unlocked, [0, 0, 1, 1])print(trace)
[(0, 'read', 'x'), (0, 'write', 'x'), (1, 'read', 'x'), (1, 'write', 'x')]
02

Lockset: which lock guards this variable?

The first detector uses a discipline: every shared variable should be guarded by some lock that is held at every access. The lockset rule tracks the locks each thread holds, and for every variable intersects the sets of locks held at its accesses. If at least two threads touched the variable, one of them wrote it, and the intersection is empty, then no single lock protects it: report a potential race. The unlocked counter is flagged; the locked one is not. The rule is cheap, and it can raise a false alarm. In the hand-over below thread 0 writes x, and then passes it to thread 1 through the lock. No lock was held at the write, so the rule complains, although the accesses are in fact ordered.

main.pyPython
def lockset_races(trace):    held = {}    accesses = {}    for tid, kind, name in trace:        locks = held.setdefault(tid, set())        if kind == "acquire":            locks.add(name)        elif kind == "release":            locks.discard(name)        else:            accesses.setdefault(name, []).append((tid, kind == "write", frozenset(locks)))    racy = []    for name, history in accesses.items():        threads = {tid for tid, _, _ in history}        writes = any(is_write for _, is_write, _ in history)        common = frozenset.intersection(*(locks for _, _, locks in history))        if len(threads) >= 2 and writes and not common:            racy.append(name)    return sorted(racy)locked = [[("acquire", "m"), ("read", "x"), ("write", "x"), ("release", "m")]] * 2guarded = interleave(locked, [0, 0, 0, 0, 1, 1, 1, 1])handoff = [(0, "write", "x"), (0, "acquire", "m"), (0, "release", "m"), (1, "acquire", "m"), (1, "release", "m"), (1, "read", "x")]print("unlocked:", lockset_races(trace))print("locked:  ", lockset_races(guarded))print("handoff: ", lockset_races(handoff))
unlocked: ['x']
locked:   []
handoff:  ['x']
DefinitionEraser lockset rule

Report a variable when two threads access it, one writes, and no lock is held at all its accesses. It finds missing locking discipline from one run, with false alarms where the order comes from something else than a common lock.

03

Happens-before: what could really overlap

The second detector asks the exact question: is there a chain of orderings from the first access to the second? Two kinds of edge exist, the order inside a thread and the pair release then acquire of the same lock. The happens-before relation is the closure of both. A vector clock per thread records what the thread has seen; a release stores the clock in the lock and an acquire takes the pointwise maximum with it. An access is a race if a conflicting earlier access by another thread has an epoch the current clock has not seen. On the hand-over, the release and the acquire connect the write to the read, so there is no race. It reports only real races, but only in the accesses it observed.

main.pyPython
def hb_races(trace):    clocks = {}    lock_clocks = {}    history = {}    racy = set()    for tid, kind, name in trace:        clock = clocks.setdefault(tid, {tid: 1})        if kind == "acquire":            for u, value in lock_clocks.get(name, {}).items():                clock[u] = max(clock.get(u, 0), value)        elif kind == "release":            lock_clocks[name] = dict(clock)            clock[tid] += 1        else:            is_write = kind == "write"            for u, was_write, epoch in history.get(name, []):                if u != tid and (is_write or was_write) and clock.get(u, 0) < epoch:                    racy.add(name)            history.setdefault(name, []).append((tid, is_write, clock[tid]))    return sorted(racy)print("unlocked:", hb_races(trace))print("locked:  ", hb_races(guarded))print("handoff: ", hb_races(handoff))
unlocked: ['x']
locked:   []
handoff:  []
Use both, in this order

Real tools such as ThreadSanitizer use happens-before because it has no false alarms on the trace it sees. The lockset rule is the cheaper way to find a variable nobody ever guards, and a good first check. Neither replaces a model checker for a lock-free structure, whose correctness does not come from locks at all.

04

How big is the space?

A model checker visits the same program, but it remembers states. In our programs a state is only how far each thread has got, so with threads of n steps there are (n1 + 1) × (n2 + 1) × ... states, however many schedules lead to them. The counter threads below give the numbers for the real data, with the schedules and the states side by side.

main.pyPython
def count_states(lengths):    total = 1    for n in lengths:        total *= n + 1    return totaldef counter_programs(ups, locked):    programs = []    for count in ups:        program = []        for _ in range(count):            program += ([("acquire", "m")] if locked else []) + [("read", "x"), ("write", "x")] + ([("release", "m")] if locked else [])        programs.append(program)    return programsreturns = [float(r) for r in load_bars()["close"].pct_change().dropna()][:6]ups = [sum(1 for r in returns[start::2] if r > 0) for start in (0, 1)]for name, lock in (("unlocked", False), ("locked", True)):    programs = counter_programs(ups, lock)    lengths = [len(p) for p in programs]    trace = interleave(programs, serial_schedule(lengths))    print(name, len(trace), "events", lockset_races(trace), hb_races(trace), count_schedules(lengths), "schedules,", count_states(lengths), "states")
unlocked 8 events ['x'] ['x'] 28 schedules, 21 states
locked 16 events [] [] 1820 schedules, 65 states
What the real data saystwo threads counting the up days of the first 6 SPY returns (1 and 3)
Unlocked counterxflagged by both detectors
Locked counternoneflagged by neither
Schedules of the locked program1,820to check one by one
States of the locked program65to remember in all

One serial trace is enough for both detectors to find the missing lock, and the locked program is clean. The model checker's advantage over brute force is the factor between 1,820 and 65, and it grows without bound: for 10 and 10 steps it is 184,756 against 121. That is why model checking can cover programs that plain enumeration never could, while a race detector costs one pass over one run.

DefinitionState space reduction

Remember a state the first time it is reached and never expand it twice. Many schedules reach the same state, so the cost drops from the number of schedules to the number of states.

05

What you will build

The lab follows the lecture: the trace of a run, the lockset rule, the happens-before rule with vector clocks, the count of states, the programs of the counting threads, and the study of both detectors and both space sizes on SPY up days.

  • Function · questionBuilds on
  • interleave(programs, schedule)What happened, in what order?Plain arithmetic
  • lockset_races(trace)Which variables have no common lock?Plain arithmetic
  • hb_races(trace)Which variables have unordered accesses?Plain arithmetic
  • count_states(lengths)How many states must be remembered?Plain arithmetic
  • counter_programs(ups, locked)What do the counting threads do?Plain arithmetic
  • study(bars)Do the detectors agree on SPY?counter_programsinterleavelockset_raceshb_racescount_states

Key takeaways

  • A detector reads one run. A serial trace in which nothing went wrong still reveals the missing lock.
  • Lockset is cheap and loud, happens-before is exact. A hand-over through a lock is a false alarm for lockset and ordered for vector clocks.
  • Remember states, not schedules. 1,820 schedules against 65 states, and 184,756 against 121 for two threads of 10 steps.
Quick check
Thread 0 writes x and then releases lock m; thread 1 acquires m and then reads x. The write is before the release. Does the vector-clock detector report a race on x?
Pick an answer.

Next topic: lock-free code has one more danger that no detector or checker can fix after the fact: when a thread frees a node that another thread is still reading. Hazard pointers and epoch reclamation build the two classic ways to free memory safely, and the explorer shows a use-after-free under every schedule that you have not prevented.

Lab · what's graded

6 tasks · 100 pts
  • The trace of a run10 pts
  • The lockset rule25 pts
  • Happens-before with vector clocks30 pts
  • The states of a model checker5 pts
  • The counting threads5 pts
  • The study on the SPY table25 pts
Explorer · lab-lf-09
▾ lab-lf-09
▾ data
…
bash · in-browser
learner@quantcraft:~/lab-lf-09$
Python not loaded yetPythonLn 1, Col 1Spaces: 4UTF-8

Reset this topic?

Everything in lab-lf-09 goes back to the starter files and the terminal is cleared. Your current solution will be lost — this can't be undone.

Will be reset
  • main.py → starter code
  • Terminal history and test output
  • Unsubmitted changes
Will be kept
  • Best lab score
  • Your XP and streak
  • Lecture progress and notes