σμ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.3·~2 hrExpert

Capstone: a lock-free market-data fan-out with proofs of its invariants

Skill positionModule 4 of 4 · Topic 3 of 3
4.3 Capstone: a lock-free market-data fan-out with proofs of its invariants
—Lab score— / 100Total XP—Streak—

4 min readCode examples: 3

  1. 01The design: a log and a cursor each
  2. 02The consumer: read the tail, then your own cursor
  3. 03Three invariants, then every schedule
  4. 04What you will build

The capstone is a fan-out: one producer publishes SPY ticks and several consumers each read all of them, with no lock anywhere. Everything you built meets here. A tail that only the producer moves, a private cursor per consumer so nobody contends, a write order that decides what a reader can see, and an explorer that turns "it seems to work" into a statement about every schedule.

01

The design: a log and a cursor each

The feed is a log that only grows. Position p holds tick<p>, and tail says how many positions are published. Only the producer writes ticks and the tail, so there is nothing to contend over on the producer side. Each consumer owns a cursor, the next position it will read, and nobody else writes it. That is the whole trick of a fan-out: the consumers never write anything the others read, so adding a consumer costs the others nothing. The one shared fact is the tail, and the one question is in which order the producer writes the tick and moves the tail. Move it after the data and a reader that sees the new tail finds the data. Move it before, and a reader can see a position that is not written yet.

main.pyPython
def publish(shared, tick, safe=True):    if safe:        yield        pos = shared["tail"]        shared["tick%d" % pos] = tick        shared["put%d" % pos] = tick        yield        shared["tail"] = pos + 1    else:        yield        pos = shared["tail"]        shared["tail"] = pos + 1        yield        shared["tick%d" % pos] = tick        shared["put%d" % pos] = tickfor safe in (True, False):    shared, _ = run_prefix(lambda sh: [publish(sh, 9369, safe)], [0], make_feed(1))    print("safe  " if safe else "unsafe", "after one turn:", {k: v for k, v in shared.items() if k in ("tail", "tick0")})
safe   after one turn: {'tail': 0, 'tick0': 9369}
unsafe after one turn: {'tail': 1}
02

The consumer: read the tail, then your own cursor

A poll takes two turns. The first reads the tail. The second reads the consumer's own cursor, and if the cursor is behind the tail it reads the tick, advances the cursor and records what it got. Nothing about the position can change under it, because only this consumer writes its cursor. What can go wrong is what the tick holds: a reader that gets in after the tail moved but before the data arrived reads nothing. The poll records that as None, so the checker can see it.

main.pyPython
def poll(shared, c):    yield    tail = shared["tail"]    yield    pos = shared["cur%d" % c]    if pos >= tail:        return None    value = shared.get("tick%d" % pos)    shared["cur%d" % c] = pos + 1    shared["got%d_%d" % (c, pos)] = value    return valueready = dict(make_feed(1), tail=1)shared, alive = run_prefix(lambda sh: [poll(sh, 0)], [0, 0], ready)print("tail is 1 but tick0 was never written:", {k: v for k, v in shared.items() if k.startswith(("cur", "got"))})
tail is 1 but tick0 was never written: {'cur0': 1, 'got0_0': None}
Why a consumer cannot just wait

A poll that finds nothing returns None at once instead of spinning until the tick appears. That keeps every thread's program finite, which is what lets the explorer finish, and it is also the lock-free contract: no thread ever depends on another thread making progress to complete its own call.

03

Three invariants, then every schedule

What does "correct" mean for the feed? Three statements about the final state. Torn: every tick a consumer received equals the tick that was put at that position (a missing tick received as None is torn). Gap: each consumer received positions 0, 1, 2, ... with no hole. Overrun: no cursor is beyond the tail. verify explores every schedule of a producer and the consumers and counts the schedules that break each one. The explorer is the proof: it cannot sample the one lucky run, it visits them all.

main.pyPython
def violations(final):    puts = {int(key[3:]): value for key, value in final.items() if key.startswith("put")}    gots = {}    for key, value in final.items():        if key.startswith("got"):            c, p = key[3:].split("_")            gots[(int(c), int(p))] = value    broken = []    if any(value != puts.get(p) for (c, p), value in gots.items()):        broken.append("torn")    for c in {c for c, _ in gots}:        if sorted(p for cc, p in gots if cc == c) != list(range(sum(1 for cc, _ in gots if cc == c))):            broken.append("gap")            break    if any(key.startswith("cur") and value > final["tail"] for key, value in final.items()):        broken.append("overrun")    return sorted(broken)def verify(ticks, polls, safe=True):    outcomes = explore_all(feed_threads(ticks, polls, safe), make_feed(len(polls)))    broken = {}    delivered = 0    for key, count in outcomes.items():        final = dict(key)        delivered = max(delivered, sum(1 for k in final if k.startswith("got")))        for name in violations(final):            broken[name] = broken.get(name, 0) + count    return {"schedules": sum(outcomes.values()), "broken": dict(sorted(broken.items())), "delivered": delivered}ticks = [9369, 9305]print("safe  ", verify(ticks, (2, 1), True))print("unsafe", verify(ticks, (2, 1), False))
safe   {'schedules': 3150, 'broken': {}, 'delivered': 3}
unsafe {'schedules': 3150, 'broken': {'torn': 869}, 'delivered': 3}
What the real data saysa producer with the first two SPY closes (9,369 and 9,305 cents), a consumer polling twice and another polling once
Schedules3,150explored for each order
Data first0schedules break an invariant
Tail first869schedules deliver a tick that is not there
A calm run2ticks received by each consumer, in order

The same program, 3,150 schedules, and a single reordering of two lines turns "none broken" into 869 broken. Most of them are schedules that a few thousand test runs would almost never hit. That is the case for exploring instead of testing, and it is the whole skill in one table: a model small enough to enumerate, a checker for the invariants, and a verdict about every schedule.

04

What you will build

The lab follows the lecture: the empty feed, the producer in its two orders, the consumer's poll, the checker of the three invariants, the verifier that explores every schedule, a replay of a calm run in which each consumer receives the ticks in order, and the study on SPY closes.

  • Function · questionBuilds on
  • make_feed(consumers)What is shared?Plain arithmetic
  • publish(shared, tick, safe)Data first, or tail firstPlain arithmetic
  • poll(shared, c)The next tick for consumer cPlain arithmetic
  • violations(final)Which invariants broke?Plain arithmetic
  • verify(ticks, polls, safe)Is the feed correct under every schedule?explore_allviolations
  • drain(ticks, polls)What does each consumer receive?run_prefix
  • study(bars)Safe, unsafe and calm on SPYverifydrain

Key takeaways

  • Fan-out scales because readers never write what others read. A private cursor per consumer means a new consumer costs the others nothing.
  • The write order is the contract. Data before the tail is safe; the tail before the data breaks 869 of 3,150 schedules.
  • Prove it over every schedule. State the invariants, explore the whole model, and read the count of broken schedules.
Quick check
In the unsafe order the producer moves the tail first. Which reader sees a torn tick?
Pick an answer.

That completes the lock-free skill. You can now build the pieces (atomics, compare-and-swap, stacks and queues), name the hazards (races, ABA, reclamation), and above all check a design against every schedule instead of trusting a run that happened to pass. The next step is the same discipline on real hardware, in the C++ atomics topics.

Lab · what's graded

7 tasks · 100 pts
  • The empty feed5 pts
  • The producer15 pts
  • The consumer20 pts
  • The invariants15 pts
  • The verifier15 pts
  • A calm run10 pts
  • The study on the SPY table20 pts
Explorer · lab-lf-11
▾ lab-lf-11
▾ data
…
bash · in-browser
learner@quantcraft:~/lab-lf-11$
Python not loaded yetPythonLn 1, Col 1Spaces: 4UTF-8

Reset this topic?

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