σμ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.1·~55分钟高级

测试并发代码:竞态检测器与模型检验

路线进度模块 4/4 · 主题 1/3
4.1 测试并发代码:竞态检测器与模型检验
—实验得分— / 100总 XP—连续天数—

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

  1. 01一条轨迹与一个问题
  2. 02Lockset:哪把锁保护这个变量?
  3. 03happens-before:什么真的可能重叠
  4. 04空间有多大?
  5. 05你要实现什么

把所有调度跑一遍可以证明一个小程序,但代价是交错的总数。真实的工具有两条捷径。竞态检测器只观察一次运行,就报告在别的调度下可能发生竞争的变量。模型检验器则记住见过的状态。两个各 10 步的线程,是 184,756 种调度对 121 个状态。

01

一条轨迹与一个问题

竞态检测器不运行线程,它读的是一条轨迹:每一次对共享数据的读和写,以及每一次对锁的获取和释放。我们的程序变成这类操作的列表,interleave 把一个程序和一个调度变成该次运行的轨迹。两个线程各自读写 x 且不用锁,就得到下面的四个事件。在这个调度里线程 0 在线程 1 开始之前就结束了,所以测试会看到正确的结果。而检测器的问题不同:这两个线程有没有可能重叠?

main.pyPython
def interleave(programs, schedule):    position = [0] * len(programs)    trace = []    for tid in schedule:        kind, name = programs[tid][position[tid]]        position[tid] += 1        trace.append((tid, kind, name))    return traceunlocked = [[("read", "x"), ("write", "x")], [("read", "x"), ("write", "x")]]trace = interleave(unlocked, [0, 0, 1, 1])print(trace)
[(0, 'read', 'x'), (0, 'write', 'x'), (1, 'read', 'x'), (1, 'write', 'x')]
02

Lockset:哪把锁保护这个变量?

第一个检测器靠一条纪律:每个共享变量都应该被某把在每次访问时都被持有的锁保护。lockset规则跟踪每个线程持有的锁,并对每个变量取其各次访问时持有的锁集合的交集。如果至少两个线程碰过这个变量,其中一个写过它,而交集为空,就说明没有任何一把锁保护它:报告一个潜在竞态。没加锁的计数器会被标出,加锁的不会。这条规则代价低,但会误报。在下面的交接里,线程 0 写 x,然后通过锁把它交给线程 1。写入时没持有任何锁,所以规则抱怨,尽管这些访问实际上是有序的。

main.pyPython
def lockset_races(trace):    held = {}    accesses = {}    for tid, kind, name in trace:        locks = held.setdefault(tid, set())        if kind == "acquire":            locks.add(name)        elif kind == "release":            locks.discard(name)        else:            accesses.setdefault(name, []).append((tid, kind == "write", frozenset(locks)))    racy = []    for name, history in accesses.items():        threads = {tid for tid, _, _ in history}        writes = any(is_write for _, is_write, _ in history)        common = frozenset.intersection(*(locks for _, _, locks in history))        if len(threads) >= 2 and writes and not common:            racy.append(name)    return sorted(racy)locked = [[("acquire", "m"), ("read", "x"), ("write", "x"), ("release", "m")]] * 2guarded = interleave(locked, [0, 0, 0, 0, 1, 1, 1, 1])handoff = [(0, "write", "x"), (0, "acquire", "m"), (0, "release", "m"), (1, "acquire", "m"), (1, "release", "m"), (1, "read", "x")]print("unlocked:", lockset_races(trace))print("locked:  ", lockset_races(guarded))print("handoff: ", lockset_races(handoff))
unlocked: ['x']
locked:   []
handoff:  ['x']
定义Eraser 的 lockset 规则

当两个线程访问某变量、其中一个写它、且所有访问时没有共同持有的锁时,就报告该变量。它能从一次运行里找出缺失的加锁纪律,但当顺序来自共同锁之外的东西时会误报。

03

happens-before:什么真的可能重叠

第二个检测器问的是准确的问题:从第一次访问到第二次访问,有没有一条有序链?边有两种:线程内部的顺序,以及同一把锁的“释放后获取”这一对。happens-before关系是这两者的闭包。每个线程一个向量时钟,记录该线程见过什么;释放时把时钟存进锁,获取时和它逐项取最大。如果另一个线程较早的冲突访问的纪元是当前时钟没见过的,这次访问就是竞态。在交接中,释放与获取把写和读连了起来,所以没有竞态。它只报告真正的竞态,但只限于它观察到的那些访问。

main.pyPython
def hb_races(trace):    clocks = {}    lock_clocks = {}    history = {}    racy = set()    for tid, kind, name in trace:        clock = clocks.setdefault(tid, {tid: 1})        if kind == "acquire":            for u, value in lock_clocks.get(name, {}).items():                clock[u] = max(clock.get(u, 0), value)        elif kind == "release":            lock_clocks[name] = dict(clock)            clock[tid] += 1        else:            is_write = kind == "write"            for u, was_write, epoch in history.get(name, []):                if u != tid and (is_write or was_write) and clock.get(u, 0) < epoch:                    racy.add(name)            history.setdefault(name, []).append((tid, is_write, clock[tid]))    return sorted(racy)print("unlocked:", hb_races(trace))print("locked:  ", hb_races(guarded))print("handoff: ", hb_races(handoff))
unlocked: ['x']
locked:   []
handoff:  []
两个都用,按这个顺序

ThreadSanitizer 这样的真实工具使用 happens-before,因为它在所见轨迹上不会误报。lockset 规则是找出“从没人保护过的变量”的更便宜办法,也是不错的第一道检查。对于正确性根本不靠锁的无锁结构,两者都替代不了模型检验器。

04

空间有多大?

模型检验器访问同一个程序,但它记住状态。在我们的程序里,状态就是每个线程走到了哪一步,所以线程各有 n 步时共有 (n1 + 1) × (n2 + 1) × ... 个状态,不管有多少种调度通向它们。下面的计数线程给出真实数据下的数字,把调度数和状态数并排摆出。

main.pyPython
def count_states(lengths):    total = 1    for n in lengths:        total *= n + 1    return totaldef counter_programs(ups, locked):    programs = []    for count in ups:        program = []        for _ in range(count):            program += ([("acquire", "m")] if locked else []) + [("read", "x"), ("write", "x")] + ([("release", "m")] if locked else [])        programs.append(program)    return programsreturns = [float(r) for r in load_bars()["close"].pct_change().dropna()][:6]ups = [sum(1 for r in returns[start::2] if r > 0) for start in (0, 1)]for name, lock in (("unlocked", False), ("locked", True)):    programs = counter_programs(ups, lock)    lengths = [len(p) for p in programs]    trace = interleave(programs, serial_schedule(lengths))    print(name, len(trace), "events", lockset_races(trace), hb_races(trace), count_schedules(lengths), "schedules,", count_states(lengths), "states")
unlocked 8 events ['x'] ['x'] 28 schedules, 21 states
locked 16 events [] [] 1820 schedules, 65 states
真实数据怎么说两个线程数 SPY 前 6 个收益率的上涨日(1 和 3)
无锁计数器x两个检测器都标出
加锁计数器none两者都不标
加锁程序的调度数1,820逐个检查
加锁程序的状态数65总共需要记住

一条串行轨迹就足以让两个检测器找出缺失的锁,而加锁的程序是干净的。模型检验器相对暴力枚举的优势就是 1,820 与 65 之间的倍数,而且无上限地增长:两个各 10 步的线程是 184,756 对 121。所以模型检验能覆盖简单枚举永远够不着的程序,而竞态检测器的代价只是对一次运行扫一遍。

定义状态空间缩减

第一次到达某个状态时记住它,之后绝不重复展开。许多调度会到达同一个状态,所以代价就从调度数降到了状态数。

05

你要实现什么

实验沿着本讲走:一次运行的轨迹、lockset 规则、用向量时钟实现的 happens-before 规则、状态数、计数线程的程序,以及对 SPY 上涨日的两个检测器和两种空间大小的研究。

  • 函数 · 问题依赖
  • interleave(programs, schedule)发生了什么,顺序如何?简单算术
  • lockset_races(trace)哪些变量没有共同的锁?简单算术
  • hb_races(trace)哪些变量有无序的访问?简单算术
  • count_states(lengths)需要记住多少个状态?简单算术
  • counter_programs(ups, locked)计数线程做什么?简单算术
  • study(bars)两个检测器在 SPY 上一致吗?counter_programsinterleavelockset_raceshb_racescount_states

要点

  • 检测器读的是一次运行。 一条什么都没出错的串行轨迹,仍然能暴露缺失的锁。
  • lockset 便宜但吵,happens-before 精确。 通过锁的交接对 lockset 是误报,对向量时钟则是有序的。
  • 记状态,不记调度。 1,820 种调度对 65 个状态;两个各 10 步的线程则是 184,756 对 121。
快速检查
线程 0 写 x 然后释放锁 m;线程 1 获取 m 然后读 x。写发生在释放之前。向量时钟检测器会报告 x 上的竞态吗?
请选择一个答案。

下一个主题:无锁代码还有一种危险,检测器和检验器都没法事后补救:一个线程释放了另一个线程还在读的节点。危险指针与纪元回收会构建两种经典的安全释放内存的办法,并用探索器展示:凡是你没有防住的调度,都会出现释放后使用。

实验:评分内容

6 个任务 · 100 分
  • 一次运行的轨迹10 分
  • lockset 规则25 分
  • 用向量时钟实现 happens-before30 分
  • 模型检验器的状态5 分
  • 计数线程5 分
  • 基于 SPY 表的研究25 分
资源管理器 · lab-lf-09
▾ lab-lf-09
▾ data
…
bash · 浏览器内
learner@quantcraft:~/lab-lf-09$
Python 尚未加载PythonLn 1, Col 1Spaces: 4UTF-8

要重置此主题吗?

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

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