3. 信号量类型

各位同学,今天我们来聊聊条件变量。说实话,我刚入行那会儿,条件变量和信号量傻傻分不清。总觉得它们都能让任务等一等、醒一醒,那有啥区别?直到我在一个传感器驱动项目里踩了坑——用信号量做数据就绪通知,结果系统跑着跑着就卡死了。后来才明白,这俩东西虽然长得像,但骨子里完全不是一回事。

3.1 条件变量到底是什么?

条件变量,说白了就是一个“等待室”。任务在某个条件不满足时,主动进去等着;等条件满足了,别人把它叫出来继续干活。

我习惯这么理解:

  • 条件变量本身不保护数据——它只管“等待”和“唤醒”这两个动作
  • 条件变量必须搭配互斥锁使用——因为你要检查的条件,通常涉及共享数据
  • 条件变量没有计数值——它不像信号量那样可以累加,它只有“等”和“叫”

你想想看,如果条件变量自己就能搞定一切,那还要互斥锁干嘛?实际上,条件变量的典型用法是:

// 伪代码:条件变量典型用法
pthread_mutex_lock(&mutex);
while (条件不满足) {
    pthread_cond_wait(&cond, &mutex);  // 等在这里,同时释放锁
}
// 条件满足了,继续干活
pthread_mutex_unlock(&mutex);

注意那个 while 循环——不是 if。为什么?因为任务被唤醒后,条件可能又被别人改了。这就是所谓的“虚假唤醒”。我在一个多传感器融合项目里就吃过这个亏,用 if 判断,结果两个任务同时醒来,数据被抢来抢去……嗯,从那以后我再也不敢用 if 了。

3.2 条件变量 vs 信号量:本质区别在哪?

很多教材喜欢列一堆对比表格,但我觉得先讲清楚一个核心区别就够了:

信号量是“资源计数”,条件变量是“状态等待”。

信号量关心“还有多少个资源可用”,条件变量关心“某个条件是否成立”。

举个例子你就明白了:

  • 信号量场景:一个缓冲区有10个空位。生产者放一个,信号量+1;消费者取一个,信号量-1。信号量的值就是空位数量。
  • 条件变量场景:一个传感器数据采集完成。驱动任务等着“数据就绪”这个条件。数据来了,条件成立,唤醒等待的任务。

看到没?信号量可以累加,条件变量不行。信号量你发10次,就能让10个任务通过;条件变量你发1次,最多唤醒1个任务(或者广播唤醒所有),但不会累加。

我整理了一个对比表,方便你对照:

对比维度 条件变量 信号量
核心语义 等待某个条件成立 管理有限资源
是否有计数值 无 有(计数信号量)
是否需要互斥锁 必须搭配 不需要(但常配合使用)
唤醒行为 signal 唤醒一个,broadcast 唤醒所有 release 释放一个资源,可能唤醒一个等待者
虚假唤醒 存在,必须用 while 检查 不存在
典型用途 数据就绪通知、状态变化通知 资源池管理、事件计数、互斥访问

3.3 为什么驱动开发中条件变量更常用?

我个人经验是,在驱动开发里,条件变量的出场率远高于信号量。原因很简单:

驱动层处理的是“事件”,不是“资源”。

比如一个 I2C 设备:

  • 你发起一次读取操作
  • 硬件开始传输数据
  • 传输完成后触发中断
  • 中断服务程序里,你要通知等待的任务“数据准备好了”

这个场景用条件变量就非常自然:

// 驱动中的条件变量用法
static struct {
    uint8_t *buffer;
    size_t   len;
    volatile bool done;
    pthread_mutex_t lock;
    pthread_cond_t  cond;
} i2c_xfer;

// 发起传输的任务
int i2c_read(uint8_t *buf, size_t len) {
    pthread_mutex_lock(&i2c_xfer.lock);
    i2c_xfer.buffer = buf;
    i2c_xfer.len    = len;
    i2c_xfer.done   = false;
    
    // 启动硬件传输
    HW_I2C_START();
    
    // 等待传输完成
    while (!i2c_xfer.done) {
        pthread_cond_wait(&i2c_xfer.cond, &i2c_xfer.lock);
    }
    pthread_mutex_unlock(&i2c_xfer.lock);
    return 0;
}

// 中断服务程序(简化)
void I2C_IRQHandler(void) {
    // ... 处理中断,数据已到 i2c_xfer.buffer ...
    pthread_mutex_lock(&i2c_xfer.lock);
    i2c_xfer.done = true;
    pthread_cond_signal(&i2c_xfer.cond);  // 唤醒等待的任务
    pthread_mutex_unlock(&i2c_xfer.lock);
}

如果用信号量实现同样的功能,你得额外维护一个“是否完成”的标志位,信号量只负责唤醒。但条件变量把“条件检查”和“等待唤醒”合在一起了,代码更简洁、更安全。

我的建议:在驱动开发中,如果遇到“等待某个状态/事件发生”的场景,优先考虑条件变量。信号量更适合“管理有限数量的资源”,比如 DMA 描述符池、缓冲区槽位等。

3.4 一个容易犯的错误

我曾经在一个项目中,用信号量来通知“数据就绪”。看起来没问题:

// 错误示范:用信号量做条件通知
sem_t data_ready;

// 生产者(中断)
sem_post(&data_ready);  // 数据就绪了

// 消费者(任务)
sem_wait(&data_ready);  // 等数据
// 处理数据...

问题在哪?如果生产者连续发了两次信号量,而消费者只处理了一次,那第二次的信号量就“攒”下来了。下次消费者等的时候,明明没有新数据,它却以为有数据,直接去读——读到的可能是旧数据,甚至空数据。

这就是信号量的“计数特性”带来的问题。条件变量没有这个困扰,因为它不计数。你发多少次 signal,它只唤醒当前正在等待的任务;如果没有任务在等,signal 就什么都不做。

避坑指南:千万不要用信号量来模拟条件变量。我曾经这么干过,结果系统出现间歇性数据错乱,查了三天才找到原因。信号量是资源计数器,条件变量是状态通知器,两者语义不同,不能混用。

3.5 知识结构图

下面这张图总结了条件变量和信号量的核心区别,以及各自的应用场景:

条件变量 vs 信号量:核心区别与适用场景 条件变量 等待某个条件成立 无计数值 需搭配互斥锁 存在虚假唤醒 状态通知 信号量 管理有限资源 有计数值 无需互斥锁 无虚假唤醒 资源计数 适用场景对比 条件变量:数据就绪、状态变化、事件通知 | 信号量:资源池、缓冲区管理、互斥访问 核心一句话 信号量关心“还有多少个资源”,条件变量关心“条件是否成立” 驱动开发中,事件通知用条件变量,资源管理用信号量 —— 两者不是替代关系,是互补关系

3.6 小结

这一节我们讲了条件变量的基础概念,以及它和信号量的本质区别。记住几个关键点:

  • 条件变量是“等待室”,信号量是“计数器”
  • 条件变量必须搭配互斥锁,信号量不需要
  • 条件变量存在虚假唤醒,必须用 while 循环检查条件
  • 驱动开发中,事件通知优先用条件变量,资源管理用信号量