σμ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 学习路线›模块 3 · 无锁队列›主题 3.2·~55分钟高级

ABA 问题与带标签指针

路线进度模块 3/4 · 主题 2/3
3.2 ABA 问题与带标签指针
—实验得分— / 100总 XP—连续天数—

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

  1. 01用比较并交换实现的栈
  2. 02ABA 调度
  3. 03给变化计数的标签
  4. 04你要实现什么

比较并交换问的是“值还是我读到的那个吗”,却问不了“它是不是被改过又改回来了”。在无锁栈上,这个盲区就是 ABA 问题:交换基于对栈的过期认识而成功,导致同一个值被返回两次。一个 pop 对阵一个不断折腾栈的线程,共 3,021 种调度,其中 21 种会破坏普通栈,而带标签的栈一种也不会被破坏。

01

用比较并交换实现的栈

无锁栈把节点放在共享字典里:节点 i 有值 val{i} 和指向下一个节点的链接 next{i},head 指向栈顶(空栈为 -1)。压入节点时,读 head,让新节点的链接指向它,再把 head 从读到的值交换成新节点;交换失败说明有人改了 head,于是重来。弹出时,读 head,读栈顶节点的链接和值,再把 head 从读到的节点交换成它的链接。赢的那次 pop 把值记到 popped 里。栈的每个操作都是生成器,线程用 yield from 执行它,它同时会交还返回值。

main.pyPython
def make_stack(values, tagged=False):    top = 0 if values else -1    shared = {"head": (top, 0) if tagged else top, "popped": ()}    for i, value in enumerate(values):        shared["val%d" % i] = value        shared["next%d" % i] = i + 1 if i + 1 < len(values) else -1    return shareddef push(shared, node, value, tagged=False):    shared["val%d" % node] = value    while True:        yield        old = shared["head"]        yield        shared["next%d" % node] = old[0] if tagged else old        yield        new = (node, old[1] + 1) if tagged else node        if compare_and_swap(shared, "head", old, new):            returndef pop(shared, tagged=False):    while True:        yield        old = shared["head"]        idx = old[0] if tagged else old        if idx == -1:            return None        yield        nxt = shared["next%d" % idx]        value = shared["val%d" % idx]        yield        new = (nxt, old[1] + 1) if tagged else nxt        if compare_and_swap(shared, "head", old, new):            shared["popped"] = shared["popped"] + (value,)            return valuedef alone(shared):    def thread():        for _ in range(3):            print("popped", (yield from pop(shared)))    return [thread()]final, alive = run_prefix(alone, [0] * 20, make_stack([10, 20]))print(final["head"], final["popped"])
popped 10
popped 20
popped None
-1 (10, 20)

单独运行时它就是个普通的栈:pop 依次返回 10、20,然后是 None,head 回到 -1。一切取决于:当另一个线程在一次 pop 的三次读取与它的交换之间干活时会发生什么。

02

ABA 调度

陷阱是这样的。受害者线程开始一次 pop:它读到 head = 0(节点 A)和链接 next0 = 1(节点 B),在交换之前被暂停。然后折腾线程弹出 A,弹出 B,再把节点 0 以新值 99 压回去,于是 head 又是 0,下面却什么也没有。受害者醒来。它的比较并交换只问 head 是否仍是 0,答案是“是”,于是交换成功,把已经不在栈里的节点 1 装成了栈顶。我们沿着调度 [0, 0]、折腾线程的九个回合、再到受害者走一遍,并用 conserved 检查结果:弹出的值加上栈里剩下的值,必须恰好等于放进去的值。

main.pyPython
def walk(shared, tagged=False):    values = []    idx = shared["head"][0] if tagged else shared["head"]    while idx != -1 and len(values) < 10:        values.append(shared["val%d" % idx])        idx = shared["next%d" % idx]    return valuesdef conserved(shared, expected, tagged=False):    return sorted(list(shared["popped"]) + walk(shared, tagged)) == sorted(expected)def aba_program(tagged=False):    def make_threads(shared):        def victim():            yield from pop(shared, tagged)        def churner():            yield from pop(shared, tagged)            yield from pop(shared, tagged)            yield from push(shared, 0, 99, tagged)        return [victim(), churner()]    return make_threadsprefix = [0, 0] + [1] * 9 + [0]final, alive = run_prefix(aba_program(), prefix, make_stack([10, 20]))print(final)print("still on the stack:", walk(final), "| conserved:", conserved(final, [10, 20, 99]))
{'head': 1, 'popped': (10, 20, 10), 'val0': 99, 'next0': -1, 'val1': 20, 'next1': -1}
still on the stack: [20] | conserved: False

受害者返回了 10,而折腾线程早已弹出过它(popped: (10, 20, 10)),head 是节点 1,可值 20 也已被弹出,所以 20 既在栈里又在 popped 里,新值 99 则丢了。conserved 说 False。一次丢失更新、一个重复的值、一个本不该存在的节点:全都来自一次对其所比较的值而言“正确”的交换。

定义ABA 问题

一个值被读作 A,随后变成 B,又在比较并交换检查它之前变回 A。交换看到 A 就成功了,尽管该值周围的状态已经变了。当节点可以被释放且其地址可以被重用时就会发生。

03

给变化计数的标签

补救办法是让头指针自带历史。在带标签指针版本里,head 是一对 (节点, 标签),每次成功的 push 或 pop 都把它换成 (新节点, 标签 + 1)。比较并交换现在同时比较两个字段,所以折腾线程留下的 head (0, 3) 不是受害者读到的 (0, 0)。受害者的交换失败,记一次重试,重新读到真实的栈,并弹出 99。同样的调度,有了标签就能正确结束。

main.pyPython
final, alive = run_prefix(aba_program(True), prefix, make_stack([10, 20], True))print(final["head"], "victim alive:", alive == [0])final, alive = run_prefix(aba_program(True), prefix + [0] * 6, make_stack([10, 20], True))print(final["popped"], walk(final, True), "| conserved:", conserved(final, [10, 20, 99], True))
(0, 3) victim alive: True
(10, 20, 99) [] | conserved: True
真实硬件用空闲位做同样的事

64 位指针很少用得上全部位,所以标签可以放在高 16 位里,或者用双倍宽度的比较并交换同时交换指针和计数器。回绕的标签理论上可能重复;另一种办法是安全内存回收,它阻止节点在还可能有人读它时被重用,这是后面一个主题的内容。

main.pyPython
values = [int(round(float(c) * 100)) for c in load_bars()["close"].iloc[:2]]for tagged in (False, True):    outcomes = explore_all(aba_program(tagged), make_stack(values, tagged))    corrupt = sum(n for key, n in outcomes.items() if not conserved(dict(key), values + [99], tagged))    print("tagged" if tagged else "plain ", sum(outcomes.values()), "schedules,", corrupt, "corrupt")
plain  3021 schedules, 21 corrupt
tagged 3021 schedules, 0 corrupt
真实数据怎么说由 SPY 前两个收盘价(9,369 和 9,305 美分)组成的栈,一个 pop 对阵一个折腾线程
调度3,021每种栈各探索
普通栈21损坏的调度:值重复或丢失
带标签的栈0损坏的调度

3,021 种里的 21 种不到 1%。在真机上跑压力测试可能跑上几天也碰不到一次,却在生产环境里失败一回。探索器把它们全部列出,而标签把它们全部消除。

04

你要实现什么

实验沿着本讲走:共享字典里的栈、带比较并交换循环的 push 和 pop(普通版与带标签版)、对栈的遍历、守恒检查、受害者与折腾线程的 ABA 程序,以及用两个 SPY 收盘价对两种栈做全调度研究。

  • 函数 · 问题依赖
  • make_stack(values, tagged)共享栈是什么样子?简单算术
  • push(shared, node, value, tagged)节点如何压入?简单算术
  • pop(shared, tagged)值如何弹出?简单算术
  • walk(shared, tagged)栈上有什么?简单算术
  • conserved(shared, expected, tagged)有没有丢失或重复?walk
  • aba_program(tagged)ABA 陷阱是什么样子?poppush
  • study(bars)标签挽救了多少种调度?make_stackaba_programconserved

要点

  • 比较并交换比较的是值,不是历史。 head 经历 A、B、A 后看起来没变,交换就在一个已经变了的栈上成功了。
  • 3,021 种里的 21 种就足够了。 低于 1% 的缺陷在压力测试里永远不出现,在探索器里必定出现。
  • 标签把 A、B、A 变成 A0、B1、A2。 head 这一对在每次成功交换时都会变,所以过期的交换一定失败。
快速检查
带标签的 head 是 (节点, 标签) 这一对,每次成功修改标签加 1。受害者读到的是 (0, 0)。之后折腾线程成功修改了三次(pop、pop、push)。现在 head 是什么,受害者的交换会成功吗?
请选择一个答案。

下一个主题:栈只有一个头。队列有两端,每端都有许多线程。多生产者多消费者队列会构建一个有界队列:生产者和消费者用 fetch-and-add 领取票号,每个槽位带一个序号,并用探索器在所有调度上检验它。

实验:评分内容

6 个任务 · 100 分
  • 共享栈10 分
  • 压入15 分
  • 弹出20 分
  • 遍历与检查20 分
  • ABA 程序10 分
  • 基于 SPY 表的研究25 分
资源管理器 · lab-lf-07
▾ lab-lf-07
▾ data
…
bash · 浏览器内
learner@quantcraft:~/lab-lf-07$
Python 尚未加载PythonLn 1, Col 1Spaces: 4UTF-8

要重置此主题吗?

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

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