回顾:并发编程

理解并发的工具

  • 线程 = 人 (大脑能完成局部存储和计算)
  • 共享内存 = 物理世界 (物理世界天生并行)
  • 一切都是状态机 (debugger & model checker)

“躲进厕所锁上门,我就把全世界人锁在了厕所外”

互斥问题:定义

互斥 (mutual exclusion),“互相排斥”

  • 实现 lock_t 数据结构和 lock/unlock API:
typedef struct {
  ...
} lock_t;
void lock(lock_t *lk);
void unlock(lock_t *lk);

一把 “排他性” 的锁——对于锁对象 lk

  • 如果某个线程持有锁,则其他线程的 lock 不能返回 (Safety)
  • 在多个线程执行 lock 时,至少有一个可以返回 (Liveness)
  • 正确处理处理器乱序、宽松内存模型和编译优化

互斥问题的经典算法

Peterson 算法

  • 包间、旗子和门上的字条
  • 假设 atomic load/store
    • 实现这个假设也不是非常容易的 (peterson.c)

因此,假设很重要

  • 不能同时读/写共享内存 (1960s) 不是一个好的假设
    • Load (环顾四周) 的时候不能写,“看一眼就把眼睛闭上”
    • Store (改变物理世界状态) 的时候不能读,“闭着眼睛动手”
    • 这是《操作系统》课
      • 更喜欢直观、简单、粗暴 (稳定)、有效的解决方法

实现互斥的基本假设

允许使用使我们可以不管一切麻烦事的原子指令

void atomic_inc(long *ptr);
int atomic_xchg(int val, int *ptr);

看起来是一个普通的函数,但假设

  • 包含一个原子指令
    • 指令的执行不能被打断
  • 包含一个 compiler barrier
    • 无论何种优化都不可越过此函数
  • 包含一个 memory fence
    • 保证处理器在 stop-the-world 前所有对内存的 store 都 “生效”
    • 即对 resume-the-world 之后的 load 可见

Atomic Exchange 实现

int xchg(int volatile *ptr, int newval) {
  int result;
  asm volatile(
    // 指令自带 memory barrier
    "lock xchgl %0, %1"
    : "+m"(*ptr), "=a"(result)
    : "1"(newval)
    // Compiler barrier
    : "memory"
  );
  return result;
}