σμ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.2·~1小时 10分钟专家

危险指针与纪元回收

路线进度模块 4/4 · 主题 2/3
4.2 危险指针与纪元回收
—实验得分— / 100总 XP—连续天数—

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

  1. 01立刻释放:不安全的弹出
  2. 02危险指针:先声明,再检查
  3. 03纪元:声明自己何时进入
  4. 04你要实现什么

无锁栈从不阻塞,但它不能把弹出的节点直接释放:另一个线程可能刚读过栈顶,正要读这个节点的 next。释放过的内存可能被复用或取消映射,所以这次读取就是释放后使用。有两种经典的办法。危险指针让线程声明自己正在用的节点;纪元让线程声明自己何时进入。两者都是等到没人可能还握着它时,才释放已摘下的节点。

01

立刻释放:不安全的弹出

模型就是 ABA 主题里的栈。线程读栈顶、读节点,然后用一次 compare-and-swap 换掉栈顶。不安全的弹出在交换成功的同一轮里释放节点。被释放的节点谁都不能碰,每当有线程读到它,read_node 就记一次释放后使用。两个线程各弹一次只有 20 种调度,探索器立刻找出坏的那些:一个线程读了栈顶,另一个弹出并释放了该节点,第一个线程随后去读它。

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

危险指针:先声明,再检查

危险指针把释放变成一次“对话”。线程在读节点之前,先把它的编号写进自己的槽位。光这样还不够,因为在读栈顶和写槽位之间,节点可能已被弹出并释放,所以线程要验证:重新读一次栈顶,如果变了就重来。弹出成功后,线程不直接释放节点,而是放进自己的退役列表并扫描:把每个线程的槽位读一遍,只释放没有任何槽位提到的退役节点。留在列表里的节点等下一次扫描。

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
定义危险指针

每个线程对“正在读的那一个节点”的声明:读之前写入,读之后验证。只有没有任何声明提到的退役节点才会被释放。

03

纪元:声明自己何时进入

基于纪元的回收声明得少得多。只有一个全局的纪元计数器。线程进入一次操作时把当前纪元写进自己的槽位,离开时写 -1,从不声明具体节点。在纪元 e 退役的节点,只可能被在纪元 e 或更早进入的线程握着,所以当操作内部的每个线程声明的纪元都大于 e 时,释放它是安全的。代价是:一个在里面待很久的线程,会拖住它进入之后退役的所有节点,而危险指针只拖住它点名的那一个节点。

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
泄漏不是 bug,卡住的线程才是

两种方案在某些调度结束时都会留下等待中的退役节点:探索器把它们报告为 pending。之后的扫描会释放它们,所以什么也没丢。纪元真正的危险是线程在操作内部停住:它永远不声明更新的纪元,于是它之后退役的节点永远不会被释放,内存不断增长。危险指针不受影响,因为停住的线程只拖住一个节点。

真实数据怎么说两个线程各从由 SPY 前两个收盘价(9,369 和 9,305 美分)组成的栈弹出一次
立刻释放206 种调度读到已释放的节点
危险指针12,122没有读到已释放节点的
纪元9,012没有读到已释放节点的
留着等待的节点1两种安全方案最多

安全是用调度数换来的,因为每一次声明、验证和扫描都是另一个线程可以插入的新步骤:20 变成了 12,122 和 9,012。这正是探索器在这里有用的原因:它不需要知道方案为什么正确,只是把每一种调度都检查一遍。

04

你要实现什么

实验沿着本讲走:带每线程一组槽位的共享状态、不安全的弹出、危险指针弹出、纪元弹出、两个不变量(没有释放后使用、没有丢失)的检查、探索某方案全部调度的验证器,以及在 SPY 收盘价上对三种方案的研究。

  • 函数 · 问题依赖
  • make_heap(values, threads)共享什么?简单算术
  • pop_unsafe(shared, tid)弹出并立刻释放简单算术
  • pop_hazard(shared, tid)声明、验证、退役、扫描简单算术
  • pop_epoch(shared, tid)进入、带纪元退役、回收简单算术
  • violations(final)哪些不变量被破坏了?简单算术
  • verify(scheme, values, pops)方案在所有调度下都安全吗?explore_allviolations
  • study(bars)哪种方案在 SPY 栈上存活?verify

要点

  • 释放是一套协议。 摘下节点并不意味着可以安全释放:可能还有人正要读它。
  • 危险指针点名一个节点,纪元点名一个时刻。 危险指针在声明之后需要验证;纪元更便宜,但卡住的线程会拖住一切。
  • 让探索器去找漏洞。 去掉验证,12,122 种调度中就有一部分会读到已释放的内存。
快速检查
线程 A 读了栈顶(节点 0)但还没声明。线程 B 弹出节点 0 并扫描。线程 A 声明节点 0 之后必须做什么?
请选择一个答案。

下一个主题:这只是个栈,而且是玩具版。毕业项目无锁的行情数据扇出会把整个技能用起来:SPY 的行情从一个生产者经由无锁结构流向多个消费者,探索器在所有调度下证明它的不变量。

实验:评分内容

7 个任务 · 100 分
  • 共享状态5 分
  • 立刻释放10 分
  • 危险指针20 分
  • 纪元20 分
  • 不变量10 分
  • 验证器15 分
  • 基于 SPY 表的研究20 分
资源管理器 · lab-lf-10
▾ lab-lf-10
▾ data
…
bash · 浏览器内
learner@quantcraft:~/lab-lf-10$
Python 尚未加载PythonLn 1, Col 1Spaces: 4UTF-8

要重置此主题吗?

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

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