自旋锁的缺陷

性能问题 (1)

  • 除了进入临界区的线程,其他处理器上的线程都在空转
  • 争抢锁的处理器越多,利用率越低
    • 4 个 CPU 运行 4 个 sum-spinlock 和 1 个 OBS
      • 任意时刻都只有一个 sum-atomic 在有效计算
    • 均分 CPU, OBS 就分不到 100% 的 CPU 了

性能问题 (2)

  • 持有自旋锁的线程可能被操作系统切换出去
    • 操作系统不 “感知” 线程在做什么
    • (但为什么不能呢?)
  • 实现 100% 的资源浪费

Scalability: 性能的新维度

同一份计算任务,时间 (CPU cycles) 和空间 (mapped memory) 会随处理器数量的增长而变化。

用自旋锁实现 sum++ 的性能问题

  • 严谨的统计很难

自旋锁的使用场景

  1. 临界区几乎不 “拥堵”
  2. 持有自旋锁时禁止执行流切换

使用场景:操作系统内核的并发数据结构 (短临界区)

  • 操作系统可以关闭中断和抢占
    • 保证锁的持有者在很短的时间内可以释放锁
  • (如果是虚拟机呢...😂)
    • PAUSE 指令会触发 VM Exit
  • 但依旧很难做好

实现线程 + 长临界区的互斥

作业那么多,与其干等 Online Judge 发布,不如把自己 (CPU) 让给其他作业 (线程) 执行?

“让” 不是 C 语言代码可以做到的 (C 代码只能执行指令)

  • 但有一种特殊的指令:syscall
  • 把锁的实现放到操作系统里就好啦
    • syscall(SYSCALL_lock, &lk);
      • 试图获得 lk,但如果失败,就切换到其他线程
    • syscall(SYSCALL_unlock, &lk);
      • 释放 lk,如果有等待锁的线程就唤醒

实现线程 + 长临界区的互斥 (cont'd)

操作系统 = 更衣室管理员

  • 先到的人 (线程)
    • 成功获得手环,进入游泳馆
    • *lk = 🔒,系统调用直接返回
  • 后到的人 (线程)
    • 不能进入游泳馆,排队等待
    • 线程放入等待队列,执行线程切换 (yield)
  • 洗完澡出来的人 (线程)
    • 交还手环给管理员;管理员把手环再交给排队的人
    • 如果等待队列不空,从等待队列中取出一个线程允许执行
    • 如果等待队列为空,*lk = ✅
  • 管理员 (OS) 使用自旋锁确保自己处理手环的过程是原子的

关于互斥的一些分析

自旋锁 (线程直接共享 locked)

  • 更快的 fast path
    • xchg 成功 → 立即进入临界区,开销很小
  • 更慢的 slow path
    • xchg 失败 → 浪费 CPU 自旋等待

互斥锁 (通过系统调用访问 locked)

  • 更经济的 slow path
    • 上锁失败线程不再占用 CPU
  • 更慢的 fast path
    • 即便上锁成功也需要进出内核 (syscall)