并发编程:从入门到放弃

人类是 sequential creature


人类是 (不轻言放弃的) sequential creature

  • 有问题,就会试着去解决
  • 手段:“回退到” 顺序执行
    • 标记若干块代码,使得这些代码一定能按某个顺序执行
    • 例如,我们可以安全地在块里记录执行的顺序

回退到顺序执行:互斥

插入 “神秘代码”,使得所有其他 “神秘代码” 都不能并发

  • 由 “神秘代码” 领导不会并发的代码 (例如 pure functions) 执行
void Tsum() {
  stop_the_world();
  // 临界区 critical section
  sum++;
  resume_the_world();
}


Stop the world 真的是可能的

  • Java 有 “stop the world GC”
  • 单个处理器可以关闭中断
  • 多个处理器也可以发送核间中断

失败的尝试

int locked = UNLOCK;

void critical_section() {
retry:
  if (locked != UNLOCK) {
    goto retry;
  }
  locked = LOCK;

  // critical section

  locked = UNLOCK;
}

和 “山寨支付宝” 完全一样的错误

  • 并发程序不能保证 load + store 的原子性

更严肃地尝试:确定假设、设计算法

假设:内存的读/写可以保证顺序、原子完成

  • val = atomic_load(ptr)
    • 看一眼某个地方的字条 (只能看到瞬间的字)
    • 刚看完就可能被改掉
  • atomic_store(ptr, val)
    • 对应往某个地方 “贴一张纸条” (必须闭眼盲贴)
    • 贴完一瞬间就可能被别人覆盖

对应于 model checker

  • 每一行可以执行一次全局变量读或写
  • 每个操作执行之后都发生 sys_sched()

正确性不明的奇怪尝试 (Peterson 算法)

A 和 B 争用厕所的包厢

  • 想进入包厢之前,A/B 都首先举起自己的旗子
    • A 往厕所门上贴上 “B 正在使用” 的标签
    • B 往厕所门上贴上 “A 正在使用” 的标签
  • 然后,如果对方举着旗,且门上的名字是对方,等待
    • 否则可以进入包厢
  • 出包厢后,放下自己的旗子 (完全不管门上的标签)

习题:证明 Peterson 算法正确,或给出反例

进入临界区的情况

  • 如果只有一个人举旗,他就可以直接进入
  • 如果两个人同时举旗,由厕所门上的标签决定谁进
    • 手快 🈶️ (被另一个人的标签覆盖)、手慢 🈚

一些具体的细节情况

  • A 看到 B 没有举旗
    • B 一定不在临界区
    • 或者 B 想进但还没来得及把 “A 正在使用” 贴在门上
  • A 看到 B 举旗子
    • A 一定已经把旗子举起来了
    • (!@^#&!%^(&^!@%#

绕来绕去很容易有错漏的情况

Prove by brute-force

  • 枚举状态机的全部状态 $(PC_1, PC_2, x, y, turn)$
  • 但手写还是很容易错啊——可执行的状态机模型有用了
void TA() { while (1) {
/* ❶ */  x = 1;
/* ❷ */  turn = B;
/* ❸ */  while (y && turn == B) ;
/* ❹ */  x = 0; } }

void TB() { while (1) {
/* ① */  y = 1;
/* ② */  turn = A;
/* ③ */  while (x && turn == A) ;
/* ④ */  y = 0; } }