σμ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.2·~1 hr 10 minExpert

Hazard pointers and epoch reclamation

Skill positionModule 4 of 4 · Topic 2 of 3
4.2 Hazard pointers and epoch reclamation
—Lab score— / 100Total XP—Streak—

4 min readCode examples: 3

  1. 01Free at once: the unsafe pop
  2. 02Hazard pointers: announce, then check
  3. 03Epochs: announce when you came in
  4. 04What you will build

A lock-free stack never blocks, but it cannot simply free a node it has popped: another thread may have read the head a moment ago and be about to read that node's next. Freed memory can be reused or unmapped, so that read is a use-after-free. Two classic cures exist. Hazard pointers let a thread announce the node it is using. Epochs let it announce when it entered. In both, an unlinked node is freed only when nobody can still hold it.

01

Free at once: the unsafe pop

The model is the stack of the ABA topic. A thread reads the head, reads the node, then swaps the head in one compare-and-swap. The unsafe pop frees the node in the same turn the swap succeeds. A node freed is a node nobody may touch, and read_node records a use-after-free whenever a thread reads one. Two threads that pop once each give only 20 schedules, and the explorer finds the bad ones at once: the schedules where one thread read the head, the other popped and freed that node, and the first then reads it.

main.pyPython
def pop_unsafe(shared, tid):    while True:        yield        idx = shared["head"]        if idx == -1:            return None        yield        nxt, value = read_node(shared, idx)        yield        if compare_and_swap(shared, "head", idx, nxt):            shared["popped"] = shared["popped"] + (value,)            free_node(shared, idx)            return valuedef two_pops(shared):    return [pop_unsafe(shared, 0), pop_unsafe(shared, 1)]outcomes = explore_all(two_pops, make_heap([9369, 9305]))hits = sum(n for key, n in outcomes.items() if dict(key)["uaf"])print(sum(outcomes.values()), "schedules,", hits, "of them read a freed node")
20 schedules, 6 of them read a freed node
02

Hazard pointers: announce, then check

A hazard pointer turns the free into a conversation. Before reading a node the thread writes its number into its slot. That alone is not enough, because the node may have been popped and freed between reading the head and writing the slot, so the thread validates: it reads the head again and starts over if it moved. After a successful pop the thread does not free the node, it puts it on its own retired list and scans: it reads every thread's slot once and frees only the retired nodes that no slot names. The nodes left on the list wait for a later scan.

main.pyPython
def pop_hazard(shared, tid):    while True:        yield        idx = shared["head"]        if idx == -1:            shared["hp%d" % tid] = -1            return None        yield        shared["hp%d" % tid] = idx        yield        if shared["head"] != idx:            continue        yield        nxt, value = read_node(shared, idx)        yield        if compare_and_swap(shared, "head", idx, nxt):            shared["popped"] = shared["popped"] + (value,)            shared["hp%d" % tid] = -1            shared["retired%d" % tid] = shared["retired%d" % tid] + (idx,)            break    yield    hazards = tuple(shared["hp%d" % t] for t in range(shared["threads"]))    yield    keep = []    for node in shared["retired%d" % tid]:        if node in hazards:            keep.append(node)        else:            free_node(shared, node)    shared["retired%d" % tid] = tuple(keep)    return valuedef two_pops(shared):    return [pop_hazard(shared, 0), pop_hazard(shared, 1)]outcomes = explore_all(two_pops, make_heap([9369, 9305]))hits = sum(n for key, n in outcomes.items() if dict(key)["uaf"])waiting = max(sum(len(dict(key)["retired%d" % t]) for t in (0, 1)) for key in outcomes)print(sum(outcomes.values()), "schedules,", hits, "read a freed node, up to", waiting, "node waiting at the end")
12122 schedules, 0 read a freed node, up to 1 node waiting at the end
DefinitionHazard pointer

A per-thread announcement of the one node being read, written before the read and validated after. A retired node is freed only when no announcement names it.

03

Epochs: announce when you came in

Epoch-based reclamation announces much less. There is one global epoch counter. A thread writes the current epoch into its slot when it enters an operation and writes -1 when it leaves, and it never announces individual nodes. A node retired in epoch e can only be held by a thread that entered at epoch e or before, so it is safe to free when every thread inside an operation announced an epoch greater than e. The price is that one thread that stays inside for a long time holds back every node retired after it entered, while a hazard pointer holds back only the one node it names.

main.pyPython
def pop_epoch(shared, tid):    yield    shared["ep%d" % tid] = shared["epoch"]    while True:        yield        idx = shared["head"]        if idx == -1:            shared["ep%d" % tid] = -1            return None        yield        nxt, value = read_node(shared, idx)        yield        if compare_and_swap(shared, "head", idx, nxt):            shared["popped"] = shared["popped"] + (value,)            shared["ep%d" % tid] = -1            shared["retired%d" % tid] = shared["retired%d" % tid] + ((idx, shared["epoch"]),)            shared["epoch"] = shared["epoch"] + 1            break    announced = []    for t in range(shared["threads"]):        yield        if shared["ep%d" % t] != -1:            announced.append(shared["ep%d" % t])    yield    keep = []    for node, epoch in shared["retired%d" % tid]:        if announced and epoch >= min(announced):            keep.append((node, epoch))        else:            free_node(shared, node)    shared["retired%d" % tid] = tuple(keep)    return valuedef two_pops(shared):    return [pop_epoch(shared, 0), pop_epoch(shared, 1)]outcomes = explore_all(two_pops, make_heap([9369, 9305]))hits = sum(n for key, n in outcomes.items() if dict(key)["uaf"])waiting = max(sum(len(dict(key)["retired%d" % t]) for t in (0, 1)) for key in outcomes)print(sum(outcomes.values()), "schedules,", hits, "read a freed node, up to", waiting, "node waiting at the end")
9012 schedules, 0 read a freed node, up to 1 node waiting at the end
A leak is not a bug, a stuck thread is

Both schemes leave retired nodes waiting at the end of some schedules: the explorer reports them as pending. They are freed by a later scan, so nothing is lost. The real danger of epochs is a thread that stalls inside an operation. It never announces a later epoch, so no node retired after it is ever freed and the memory grows. Hazard pointers are immune, because a stalled thread holds only one node.

What the real data saystwo threads popping once each from a stack of the first two SPY closes (9,369 and 9,305 cents)
Free at once206 schedules read a freed node
Hazard pointers12,122none read a freed node
Epochs9,012none read a freed node
Nodes left waiting1at most, for both safe schemes

Safety costs schedules, because every announcement, validation and scan is another step the other thread can slip into: 20 became 12,122 and 9,012. That is exactly why the explorer is useful here. It does not need to know why the scheme is right, it checks every one of those schedules.

04

What you will build

The lab follows the lecture: the shared state with one slot set per thread, the unsafe pop, the hazard-pointer pop, the epoch pop, the check of the two invariants (no use-after-free, nothing lost), the verifier that explores every schedule of a scheme, and the study of all three schemes on SPY closes.

  • Function · questionBuilds on
  • make_heap(values, threads)What is shared?Plain arithmetic
  • pop_unsafe(shared, tid)Pop and free at oncePlain arithmetic
  • pop_hazard(shared, tid)Announce, validate, retire, scanPlain arithmetic
  • pop_epoch(shared, tid)Enter, retire with the epoch, reclaimPlain arithmetic
  • violations(final)Which invariants broke?Plain arithmetic
  • verify(scheme, values, pops)Is a scheme safe under every schedule?explore_allviolations
  • study(bars)Which scheme survives SPY's stack?verify

Key takeaways

  • Freeing is a protocol. Unlinking a node does not make it safe to free: someone may still be about to read it.
  • A hazard pointer names one node; an epoch names a time. Hazard pointers need a validation after the announcement; epochs are cheaper but a stalled thread holds back everything.
  • Let the explorer find the hole. Remove the validation and 12,122 schedules turn into schedules that read freed memory.
Quick check
Thread A has read the head (node 0) but has not yet announced it. Thread B pops node 0 and scans. What must thread A do after it announces node 0?
Pick an answer.

Next topic: this was a stack, and a toy one. The capstone a lock-free market-data fan-out puts the whole skill to work: SPY ticks flow from one producer to several consumers through a lock-free structure, and the explorer proves its invariants over every schedule.

Lab · what's graded

7 tasks · 100 pts
  • The shared state5 pts
  • Free at once10 pts
  • Hazard pointers20 pts
  • Epochs20 pts
  • The invariants10 pts
  • The verifier15 pts
  • The study on the SPY table20 pts
Explorer · lab-lf-10
▾ lab-lf-10
▾ data
…
bash · in-browser
learner@quantcraft:~/lab-lf-10$
Python not loaded yetPythonLn 1, Col 1Spaces: 4UTF-8

Reset this topic?

Everything in lab-lf-10 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