信号量基础 (Semaphore Basics)

  • 定义 (Definition): 一种抽象的整数计数器 (Integer Counter), 用于控制多线程对共享资源的访问.
  • 核心操作 (Core Operations):
    • down() (或 P()/wait()): 原子性地减少计数器值; 若值为0则阻塞线程.
    • up() (或 V()/signal()): 原子性地增加计数器值; 唤醒等待的线程.
  • 类型 (Types):
    • 二进制信号量 (Binary Semaphore): 值仅0或1, 初始化为 1 的情况下等效于锁 (Lock).
    • 计数信号量 (Counting Semaphore): 值可为任意非负整数, 表示资源的可用数量.

信号量的应用场景 (Applications of Semaphores)

  • 互斥 (Mutual Exclusion):

    1
    2
    3
    4
    semaphore sem(1);
    sem.down();
    // 临界区 (Critical Section)
    sem.up();
  • 顺序约束 (Ordering Constraints):

    • 例如确保线程A先执行任务再启动线程B, 功能类似cv:

      1
      2
      3
      4
      5
      6
      7
      semaphore sem(0);
      // 线程A
      taskA();
      sem.up();
      // 线程B
      sem.down();
      taskB();

生产者-消费者问题 (Producer-Consumer Problem)

  • 场景 (Setup):

    • 生产者 (Producer) 填充缓冲区, 消费者 (Consumer) 清空缓冲区.
    • 缓冲区容量有限 (MAX), 需同步生产与消费速率.
  • 信号量方案 (Semaphore Solution):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    semaphore mtx(1);          // 互斥锁 (Mutual Exclusion)
    semaphore fullSlots(0); // 已填充的槽数
    semaphore emptySlots(MAX); // 空闲槽数

    // 生产者
    producer() {
    emptySlots.down(); // 等待空闲槽
    mtx.down();
    // 生产数据到缓冲区
    mtx.up();
    fullSlots.up(); // 通知消费者
    }

    // 消费者
    consumer() {
    fullSlots.down(); // 等待已填充槽
    mtx.down();
    // 消费数据
    mtx.up();
    emptySlots.up(); // 通知生产者
    }
  • 关键问题 (Key Issues):

    • 操作顺序 (Order of Operations): 若先获取互斥锁 (mtx.down()) 再等待资源 (emptySlots.down()), 则在减少资源之后本线程 sleep 而没有释放 mtx (忽略了 cv 条件中 release lock, put the thread onto waiting set, sleep 的三合一条件)
    • 信号量分离 (Separate Semaphores): fullSlotsemptySlots 需独立管理, 以处理不对称的等待条件.

信号量与监视器的对比 (Semaphores vs. Monitors)

  • 监视器 (Monitors):
    • 使用锁 (Lock) 实现互斥.
    • 使用条件变量 (Condition Variables) 实现顺序约束.
    • 需显式管理条件判断 (如 while (condition)).
  • 信号量 (Semaphores):
    • 单一机制同时处理互斥和顺序约束.
    • 更简洁但易出错 (如错误操作顺序导致死锁).
  • 典型区别 (Key Differences):
    • 信号量的历史性 (History): up() 操作会被记录, 后续 down() 可直接通过. 条件变量的无记忆性 (No Memory): signal() 若在 wait() 前调用则无效. (见下一个小节例子)
    • semaphore down 不能位于 mutex 锁内部: 信号量的 down 操作会导致线程阻塞, 但是在 mutex 锁内部会导致死锁. (见上一个小节的关键问题部分)

自定义等待条件的实现 (Implementing Custom Waiting Conditions)

  • 挑战 (Challenge): 信号量原生仅支持基于计数器值的等待 (value == 0).

  • 解决方案 (Solution): 用信号量模拟条件变量:

    • 等待队列 (Waiting Queue): 使用集合 (Set) 存储等待线程的私有信号量.

    • 示例代码 (Example):

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      semaphore cokeLock(1);          // 互斥锁
      set<sem*> waitingProducers; // 等待的生产者队列
      set<sem*> waitingConsumers; // 等待的消费者队列

      // 生产者
      producer() {
      cokeLock.down();
      while (numCokes == MAX) {
      sem* s = new sem(0);
      waitingProducers.insert(s);
      cokeLock.up();
      s->down(); // 阻塞等待
      cokeLock.down();
      }
      numCokes++;
      if (!waitingConsumers.empty()) {
      waitingConsumers.front()->up(); // 唤醒消费者
      waitingConsumers.erase(...);
      }
      cokeLock.up();
      }

[!Potential Problem] > 中断风险 (Interruption Risk): 在释放锁 (cokeLock.up()) 和等待 (s->down()) 之间可能被中断.
由于 semaphore 具有记忆功能, 在这里发生中断转移到 customer 线程之后, customer
会购买一个 coke 从而导致对应的 s->up(), 而导致返回之后这里的 s->down() 会立即返回, 且结果上没有逻辑错误.


核心总结 (Key Takeaways)

  • 信号量通过原子计数器同步线程, 支持互斥和顺序约束.
  • 生产者-消费者问题需分离资源计数与互斥锁.
  • 信号量比监视器更灵活但更易出错, 需谨慎设计操作顺序.
  • 自定义等待条件可通过信号量队列实现, 但需处理复杂性和潜在风险.