信号量的两种典型应用

  1. 实现一次临时的 happens-before
    • 初始:s = 0
    • A; V(s)
    • P(s); B
      • 假设 s 只被使用一次,保证 A happens-before B
  2. 实现计数型的同步
    • 初始:done = 0
    • Tworker: V(done)
    • Tmain: P(done) $\times T$

对应了两种线程 join 的方法

  • $T_1 \to T_2 \to \ldots$ v.s. 完成就行,不管顺序

例子:实现计算图

对于任何计算图

  • 为每个节点分配一个线程
    • 对每条入边执行 P (wait) 操作
    • 完成计算任务
    • 对每条出边执行 V (post/signal) 操作
      • 每条边恰好 P 一次、V 一次
      • PLCS 直接就解决了啊?
void Tworker_d() {
  P(bd); P(ad); P(cd);
  // 完成节点 d 上的计算任务
  V(de);
}

实现计算图 (cont'd)

乍一看很厉害

  • 完美解决了并行问题

实际上……

  • 创建那么多线程和那么多信号量 = Time Limit Exceeded
  • 解决线程太多的问题
    • 一个线程负责多个节点的计算
      • 静态划分 → 覆盖问题
      • 动态调度 → 又变回了生产者-消费者
  • 解决信号量太多的问题
    • 计算节点共享信号量
      • 可能出现 “假唤醒” → 又变回了条件变量

例子:毫无意义的练习题

有三种线程

  • Ta 若干: 死循环打印 <
  • Tb 若干: 死循环打印 >
  • Tc 若干: 死循环打印 _
  • 如何同步这些线程,保证打印出 <><_><>_ 的序列?

信号量的困难

  • 上一条鱼打印后,<> 都是可行的
  • 我应该 P 哪个信号量?
    • 可以 P 我自己的
    • 由打印 _ 的线程随机选一个

例子:使用信号量实现条件变量

当然是问 AI 了

  • ChatGPT (GPT-3.5) 直接一本正经胡说八道
    • 这个对 LLM 还是太困难了
  • New Bing 给出了一种 “思路”
    • 第一个 wait 的线程会在持有 mutex 的情况下 P(cond)
    • 从此再也没有人能获得互斥锁……
      • 像极了我改期末试卷的体验

使用信号量实现条件变量:本质困难

操作系统用自旋锁保证 wait 的原子性

wait(cv, mutex) {
  release(mutex);
  sleep();
}

信号量实现的矛盾

  • 不能带着锁睡眠 (NewBing 犯的错误)
  • 也不能先释放锁
    • P(mutex); nwait++; V(mutex);
    • 此时 signal/broadcast 发生,唤醒了后 wait 的线程
    • P(sleep);
  • (我们稍后介绍解决这种矛盾的方法)

信号量的使用:小结

信号量是对 “袋子和球/手环” 的抽象

  • 实现一次 happens-before,或是计数型的同步
    • 能够写出优雅的代码
    • P(empty); printf("("); V(fill)
  • 但并不是所有的同步条件都容易用这个抽象来表达