伤人性命的并发 Bug

伤人性命的并发 Bug

2025 南京大学《操作系统原理》
伤人性命的并发 Bug

“Killed by a Machine”

Therac-25 Incident (1985-1987)

  • 事件驱动导致的并发 bug,导致至少 6 人死亡

center

2025 南京大学《操作系统原理》
伤人性命的并发 Bug

The Therac-25

center

assert mode in [Electron(Low), XRay(High)]
assert beam_flattener in [On, Off]
assert not (mode == XRay(High) and beam_flattener == Off)
2025 南京大学《操作系统原理》
伤人性命的并发 Bug

The Killer Software Bug in History

“支付宝” 的案例再现

  • 在 Electron (Low) Mode 下选择 X-Ray (High) Mode
    • 机器开始移动 beam flattener,大约需要 8s 完成...
    • 切换到 Electron (Low) Mode (OK)
    • 迅速切换到 X-Ray (High) Mode
    • 此时触发 Assertion fail (Malfunction 54)
      • 操作员下意识地按下 Continue……

“软件定义” 带来的悲剧后果

  • 在更早的产品 (Therac-20) 中,assert 由电路互锁 (interlock) 强制实现,直接停机 (需要手工重启)
2025 南京大学《操作系统原理》
伤人性命的并发 Bug

这甚至不是 Therac-25 的最后一个杀人 Bug

问题修复后……

  • If the operator sent a command at the exact moment the counter overflowed, the machine would skip setting up some of the beam accessories

最终解决方法

  • 独立的硬件安全方案,检测到大计量照射时直接停机

“软件定义” 时代的安全问题

  • 自动驾驶、具身智能的 “底线安全” 越来越难
2025 南京大学《操作系统原理》
伤人性命的并发 Bug

Root Cause 分析

顺序程序

  • 状态修改立即生效
    • f() 返回就完成所有的状态修改
    • 如果模式切换瞬间完成,就没有任何问题了

非确定性带来的麻烦

  • 切换到 X-Ray (High) Mode 需要时间
    • beam flattener 未来才就绪
    • 但我们已经形成了 “立即生效” 的肌肉记忆
  • 函数式编程:这个我会!
    • 没有副作用 (pure functions),就没有并发问题
2025 南京大学《操作系统原理》
伤人性命的并发 Bug

数据竞争 (Data Race)

不同的线程同时访问同一内存,且至少有一个是写

  • 两个内存访问在 “赛跑”,“跑赢” 的操作先执行
    • 共享内存上实现的 Peterson 算法
    • Therac-25 事件中修改的状态 (“Race Condition”)

center

2025 南京大学《操作系统原理》
伤人性命的并发 Bug

数据竞争 (cont'd)

“跑赢” 并没有想象中那么简单

center

2025 南京大学《操作系统原理》
伤人性命的并发 Bug

我们写了很多 Data Race 的代码

Ad-hoc synchronization 都是 “Data Race”

  • 并不是说绝对不能用
    • (初学者还是算了)
void T_sum() { sum++; }
void T_peterson() {
    b = 1;
    turn = A;
    while (1) {
        if (!a) break;
        if (turn != A) break;
    }
}
  • 消灭了数据竞争 \to 保证 serializability
    • 可能竞争的内存访问要么互斥,要么同步 (回到顺序执行)
2025 南京大学《操作系统原理》
伤人性命的并发 Bug

数据竞争:例子

以下代码概括了你们遇到数据竞争的大部分情况

  • 不要笑,你们的 bug 几乎都是这两种情况的变种

Case 1: 上错了锁

void T_1() { spin_lock(&A); sum++; spin_unlock(&A); }
void T_2() { spin_lock(&B); sum++; spin_unlock(&B); }

Case 2: 忘记上锁

void T_1() { spin_lock(&A); sum++; spin_unlock(&A); }
void T_2() { sum++; }
2025 南京大学《操作系统原理》
伤人性命的并发 Bug

为什么不要笑?

实际系统面临更复杂的情况

  • “内存” 可以是地址空间中的任何内存
    • 可以是全部变量
    • 可以是堆区分配的变量
    • 可以是栈
  • “访问” 可以是任何代码
    • 可能发生在你的代码里
    • 可以发生在框架代码里
    • 可能是一行你没有读到过的汇编代码
    • 可能是一条 ret 指令
2025 南京大学《操作系统原理》