状态机的隐含假设

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

  • 没有其他任何人能 “干涉” 程序的状态
  • 推论:对变量的 load 一定返回本线程最后一次 store 的值
    • 这也是编译优化的基本假设

共享内存推翻了这个假设

int Tworker() {
  printf("%d\n", x);  // Global x
  printf("%d\n", x);
}
  • 其他线程随时可以修改 x
    • 导致两次可能读到不同的 x

潘多拉的魔盒已经打开……

unsigned int balance = 100;

int Talipay_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

例子:Diablo I (1996)

在捡起要复制物品的瞬间拿起 1 块钱

  • 1 块钱会被 “覆盖” 成捡起的物品

例子:求和

分两个线程,计算 $1+1+1+\ldots+1$ (共计 $2n$ 个 $1$)

#define N 100000000
long sum = 0;

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

int main() {
  create(Tsum);
  create(Tsum);
  join();
  printf("sum = %ld\n", sum);
}

可能的结果

  • 119790390, 99872322 (结果可以比 N 还要小), ...
  • 直接使用汇编指令也不行

放弃 (1):指令/代码执行原子性假设

“处理器一次执行一条指令” 的基本假设在今天的计算机系统上不再成立 (我们的模型作出了简化的假设)。

单处理器多线程

  • 线程在运行时可能被中断,切换到另一个线程执行

多处理器多线程

  • 线程根本就是并行执行的

(历史) 1960s,大家争先在共享内存上实现原子性 (互斥)

  • 但几乎所有的实现都是错的,直到 Dekker's Algorithm,还只能保证两个线程的互斥

放弃原子性假设的后果

printf 还能在多线程程序里调用吗?

void thread1() { while (1) { printf("a"); } }
void thread2() { while (1) { printf("b"); } }

我们都知道 printf 是有缓冲区的 (为什么?)

  • 如果执行 buf[pos++] = ch (pos 共享) 不就 💥 了吗?

RTFM!