放弃 (1)

放弃 (1)

2024 南京大学《操作系统:设计与实现》
放弃 (1)

反思:状态机模型的隐含假设

“世界上只有一个状态机”

  • 没有其他任何人能 “干涉” 程序的状态
  • 课堂上常用的简化方法
    • 假设一段程序执行没有系统调用
    • 可以直接简化为一个原子的状态迁移 “计算”
2024 南京大学《操作系统:设计与实现》
放弃 (1)

放弃:状态迁移原子性的假设

共享内存推翻了 “原子性” 假设

  • 任何时候,load 读到的值都可能是别的线程写入的
  • 我们习以为常的简化会漏掉并发程序可能的行为
    • 如果你觉得你可能会犯错误,那别人也一定会的

一些我们见到过的例子

  • 线程的交错执行 ABABAABB
  • 消失的 1: A2B2A3A5B5B6
  • 潘多拉的魔盒已经打开……
2024 南京大学《操作系统:设计与实现》
放弃 (1)

并发 Bugs 来啦!

unsigned int balance = 100;

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

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

  • 账户里会多出用不完的钱!
  • Bug/漏洞不跟你开玩笑:Mt. Gox Hack 损失 650,000 BTC
    • 时值 ~$28,000,000,000
2024 南京大学《操作系统:设计与实现》
放弃 (1)

真实的例子:Diablo I (1996)

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

2024 南京大学《操作系统:设计与实现》
放弃 (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);
}
  • 会得到怎样的结果?
2024 南京大学《操作系统:设计与实现》
放弃 (1)

一些历史

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

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

感到脊背发凉?

  • printf 还能在多线程程序里调用吗?
  • 我们都知道 printf 是有缓冲区的 (为什么?)
    • 如果执行 buf[pos++] = ch 不就 💥 了吗?
    • 消失的 1: A2B2A3A5B5B6
2024 南京大学《操作系统:设计与实现》
放弃 (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);
    }
}
2024 南京大学《操作系统:设计与实现》
放弃 (1)

答案到底是多少呢?

Model Checker: sum = 2

  • 是的,不是 1
    • (因为有 i 的循环)
  • 也不是 3
    • 虽然 sum = 3 是很容易想到的
  • 无论有多少 T_sum,都可以 sum = 2

GPT-4 的 “直觉” 哪怕对最 “简单” 的并发程序都不起效

2024 南京大学《操作系统:设计与实现》
放弃 (1)

开始理解 “数学视角” 的价值

对于并发,讲概念是不够的

  • 事实可能不是你想的那样

甚至讲代码都是不够的

  • 代码需要非常精巧的 workload 才能跑出那个 corner case

证明才是解决问题的方法

证明:\forall 线程调度方法,程序满足 XXX 性质。

  • 我们现在甚至还没有趁手的并发程序证明工具!
  • 对于课堂的例子,model checker 倒也够用了
2024 南京大学《操作系统:设计与实现》