σμ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 Academy路线图学习路线
Ctrl K
AO
+1.2k 位学习者
登录加入 ↗
路线图学习路线
AO
+1.2k 位学习者
登录
  • 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 Academy路线图学习路线
Ctrl K
AO
+1.2k 位学习者
登录加入 ↗
路线图学习路线
AO
+1.2k 位学习者
登录
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
Lock-free 学习路线›模块 4 · 测试与内存安全›主题 4.3·~2小时专家

结业项目:无锁行情分发及其不变量的证明

路线进度模块 4/4 · 主题 3/3
4.3 结业项目:无锁行情分发及其不变量的证明
—实验得分— / 100总 XP—连续天数—

阅读约 4 分钟3 个代码示例

  1. 01设计:一个日志,各有一个游标
  2. 02消费者:先读尾指针,再读自己的游标
  3. 03三个不变量,然后是每一种调度
  4. 04你要实现什么

毕业项目是一个扇出:一个生产者发布 SPY 行情,多个消费者各自读到全部,全程没有任何锁。你建过的东西在这里汇合:只有生产者才会移动的尾指针、每个消费者一个私有游标因此互不争抢、决定读者能看到什么的写入顺序,以及把“看起来能用”变成“对每一种调度都成立”的探索器。

01

设计:一个日志,各有一个游标

这个数据流是一个只会增长的日志。位置 p 存放 tick<p>,tail 表示已发布多少个位置。只有生产者写行情和尾指针,所以生产者这一侧没有任何争用。每个消费者拥有一个游标,即它下一个要读的位置,别人都不写它。这正是扇出的全部窍门:消费者从不写别人会读的东西,所以多加一个消费者不会给其他人增加任何代价。唯一共享的事实是尾指针,唯一的问题是生产者以什么顺序写行情和移动尾指针。先写数据再移动,看到新尾指针的读者就能找到数据;先移动再写,读者就可能看到一个还没写好的位置。

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

消费者:先读尾指针,再读自己的游标

一次轮询分两轮。第一轮读尾指针。第二轮读消费者自己的游标,若游标落后于尾指针,就读行情、推进游标并记录读到的内容。位置不会在它脚下变化,因为只有这个消费者写自己的游标。可能出错的是行情的内容:在尾指针已移动、数据尚未到达之间进来的读者什么也读不到。轮询把这记为 None,让检查器能看见。

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}
为什么消费者不能干脆等着

什么也没找到的轮询会立刻返回 None,而不是空转到行情出现。这让每个线程的程序保持有限,探索器才能结束;这同时也是无锁的契约:没有线程需要依赖另一个线程的进展才能完成自己的调用。

03

三个不变量,然后是每一种调度

对这个数据流来说“正确”是什么意思?关于最终状态的三条陈述。撕裂(torn):消费者收到的每条行情都等于放在该位置的行情(缺失的行情被收成 None 就算撕裂)。空洞(gap):每个消费者收到的位置是 0、1、2、……没有空洞。越界(overrun):没有游标超过尾指针。verify 探索一个生产者加若干消费者的每一种调度,并数出破坏每条不变量的调度数。探索器就是证明:它不是抽样那一次幸运的运行,而是把所有运行都访问一遍。

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}
真实数据怎么说一个带有 SPY 前两个收盘价(9,369 和 9,305 美分)的生产者,一个轮询两次的消费者和一个轮询一次的消费者
调度数3,150每种顺序都探索
先写数据0种调度破坏不变量
先移尾指针869种调度交付了不存在的行情
平静的运行2每个消费者按序收到的行情

同一个程序、3,150 种调度,只把两行的顺序对调一下,结果就从“没有破坏”变成了 869 种破坏。其中大多数是几千次测试运行也几乎碰不到的调度。这就是用探索代替测试的理由,也是整个技能浓缩成一张表:一个小到可以枚举的模型、一个检查不变量的检查器,以及对每一种调度的裁决。

04

你要实现什么

实验沿着本讲走:空的数据流、两种顺序的生产者、消费者的轮询、三个不变量的检查器、探索全部调度的验证器、各消费者按序收到行情的平静运行回放,以及在 SPY 收盘价上的研究。

  • 函数 · 问题依赖
  • make_feed(consumers)共享什么?简单算术
  • publish(shared, tick, safe)先写数据还是先移尾指针简单算术
  • poll(shared, c)消费者 c 的下一条行情简单算术
  • violations(final)哪些不变量被破坏了?简单算术
  • verify(ticks, polls, safe)数据流在所有调度下都正确吗?explore_allviolations
  • drain(ticks, polls)每个消费者收到什么?run_prefix
  • study(bars)SPY 上的安全、不安全与平静verifydrain

要点

  • 扇出之所以能扩展,是因为读者从不写别人要读的东西。 每个消费者一个私有游标,新增消费者不会给其他人增加代价。
  • 写入顺序就是契约。 数据先于尾指针是安全的;尾指针先于数据会破坏 3,150 种调度中的 869 种。
  • 对每一种调度证明它。 写出不变量,探索整个模型,读出被破坏的调度数。
快速检查
在不安全的顺序里,生产者先移动尾指针。哪个读者会看到撕裂的行情?
请选择一个答案。

无锁技能到此完成。你现在能构建各个部件(原子操作、compare-and-swap、栈和队列),点出各种危险(竞态、ABA、回收),最重要的是能把设计对每一种调度逐一检查,而不是相信一次碰巧通过的运行。下一步是在真实硬件上沿用同样的纪律,也就是 C++ 原子操作的主题。

实验:评分内容

7 个任务 · 100 分
  • 空的数据流5 分
  • 生产者15 分
  • 消费者20 分
  • 不变量15 分
  • 验证器15 分
  • 平静的运行10 分
  • 基于 SPY 表的研究20 分
资源管理器 · lab-lf-11
▾ lab-lf-11
▾ data
…
bash · 浏览器内
learner@quantcraft:~/lab-lf-11$
Python 尚未加载PythonLn 1, Col 1Spaces: 4UTF-8

要重置此主题吗?

lab-lf-11 中的所有内容将恢复为初始文件,终端也会被清空。你当前的解答将丢失,且无法撤销。

将被重置
  • main.py → 初始代码
  • 终端历史与测试输出
  • 未提交的更改
将被保留
  • 实验最高得分
  • 你的 XP 与连续天数
  • 讲解进度与笔记