放弃 (1):状态迁移的确定性

放弃 (1):状态迁移的确定性

2025 南京大学《操作系统原理》
放弃 (1):状态迁移的确定性

确定性的丧失

虚拟化使进程认为 “世界上只有自己”

  • 除了系统调用,程序的行为是 deterministic 的
    • 初始状态 (argv, envp) 一样、syscall 行为一样,程序无论运行多少次,结果都是一样的

并发打破了这一点

  • 并发程序每次 non-deterministically 选一个线程执行
    • 这意味着 load 可能读到其他线程的 store (也可能不)
  • 非确定性的程序理解起来相当困难
2025 南京大学《操作系统原理》
放弃 (1):状态迁移的确定性

确定性的丧失:例子

unsigned int balance = 100;

int T_alipay_withdraw(int amount) {
    if (balance >= amount) {
        balance -= amount;
        return SUCCESS;
    } else {
        return FAIL;
    }
}

两个线程并发支付 ¥100 会发生什么 (代码演示)

  • 账户里会多出用不完的钱!
  • Bug/漏洞不跟你开玩笑:Mt. Gox Hack 损失 650,000 BTC
    • 时值 ~$28,000,000,000
2025 南京大学《操作系统原理》
放弃 (1):状态迁移的确定性

真实的例子:Diablo I (1996)

在捡起物品的瞬间拿起 1 块钱……

2025 南京大学《操作系统原理》
放弃 (1):状态迁移的确定性

你发现 1 + 1 都不会求了……

计算 1+1+1++11+1+1+\ldots+1

  • 共计 2n2n11,分 2 个线程计算
#define N 100000000
long sum = 0;

void T_sum() { for (int i = 0; i < N; i++) sum++; }

int main() {
    create(T_sum);
    create(T_sum);
    join();
    printf("sum = %ld\n", sum);
}
  • 会得到怎样的结果?
2025 南京大学《操作系统原理》
放弃 (1):状态迁移的确定性

失去确定性的后果

并发执行三个 T_sum,sum 的最小值是多少?

  • 初始时 sum = 0; 假设单行语句的执行是原子的
void T_sum() {
    for (int i = 0; i < 3; i++) {
        int t = load(sum);
        t += 1;
        store(sum, t);
    }
}
  • deepseek-r1 & o3-mini: 3
    • 即便给 “还有更小的” 的提示,r1 和 o3-mini 都在非常长的思考后……还是给出 3
2025 南京大学《操作系统原理》
放弃 (1):状态迁移的确定性

答案到底是多少呢?

Model Checker: sum = 2

“数学视角” 的价值

  • Nondeterminism 对人类来说是本质困难
  • 证明才是解决问题的方法
    • \forall 线程调度,程序满足 XXX 性质
2025 南京大学《操作系统原理》
放弃 (1):状态迁移的确定性

确定性的丧失:后果

正确实现并发 1 + 1 比想象中困难得多

  • 1960s,大家争先在共享内存上实现原子性 (互斥)
  • 但几乎所有的实现都是错的

并发影响了计算机系统世界中的一切

  • libc 里的函数还能在多线程程序里调用吗?
  • 我们都知道 printf 是有缓冲区的 (fork 的例子)
    • 两个线程同时执行 buf[pos++] = ch 很危险
    • man 3 printf
2025 南京大学《操作系统原理》