在C++并发编程中,信号量和条件变量是实现线程间同步与协作的重要工具,两者都能协调多个线程的执行顺序,但适用场景和实现逻辑存在差异,掌握它们的用法是编写正确并发程序的基础。

C++信号量的用法
C++20标准正式将信号量纳入标准库,定义在<semaphore>头文件中,主要包含counting_semaphore和binary_semaphore两种类型,前者是计数信号量,后者是仅能取0和1的二元信号量,本质是计数信号量的特化。
信号量的核心操作
信号量的两个核心操作是acquire和release:acquire操作会尝试获取信号量,若当前信号量计数大于0则计数减1并继续执行,否则阻塞当前线程直到计数大于0;release操作会将信号量计数加1,若有阻塞的线程则唤醒其中一个。
信号量使用示例
下面是一个使用二元信号量实现线程交替执行的示例:
#include <iostream>
#include <thread>
#include <semaphore>
#include <vector>
// 定义二元信号量,初始值为1,允许第一个线程先执行
std::binary_semaphore sem1(1);
// 定义二元信号量,初始值为0,第二个线程初始需要等待
std::binary_semaphore sem2(0);
void thread_func1() {
for (int i = 0; i < 3; ++i) {
// 获取sem1信号量
sem1.acquire();
std::cout << "线程1执行第" << i + 1 << "次" << std::endl;
// 释放sem2信号量,唤醒线程2
sem2.release();
}
}
void thread_func2() {
for (int i = 0; i < 3; ++i) {
// 获取sem2信号量,初始时阻塞等待线程1释放
sem2.acquire();
std::cout << "线程2执行第" << i + 1 << "次" << std::endl;
// 释放sem1信号量,唤醒线程1
sem1.release();
}
}
int main() {
std::thread t1(thread_func1);
std::thread t2(thread_func2);
t1.join();
t2.join();
return 0;
}
运行上述代码可以看到线程1和线程2交替执行,不会出现执行顺序混乱的情况,这就是信号量控制线程执行顺序的效果。
C++条件变量的用法
条件变量定义在<condition_variable>头文件中,需要和互斥锁配合使用,主要用于让线程在某个条件不满足时阻塞等待,直到其他线程修改条件并通知该线程。
条件变量的核心组件
std::condition_variable:条件变量本身,用于线程的等待和通知std::mutex:互斥锁,用于保护共享条件和等待过程的原子性- 等待条件:线程阻塞时需要判断的条件,通常需要配合
wait方法的谓词参数使用,避免虚假唤醒
条件变量的使用步骤
- 定义互斥锁和条件变量对象
- 线程等待时,先获取互斥锁,然后调用条件变量的
wait方法,传入互斥锁和判断条件的谓词 - 其他线程修改共享条件后,获取互斥锁,修改条件并调用条件变量的
notify_one或notify_all方法通知等待的线程 - 被唤醒的线程会重新检查条件,条件满足则继续执行,否则继续等待
条件变量使用示例
下面是使用条件变量实现简单生产者消费者模型的示例:
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
std::queue<int> data_queue; // 共享队列
std::mutex mtx; // 互斥锁
std::condition_variable cv; // 条件变量
bool finished = false; // 生产结束标志
// 生产者线程函数
void producer() {
for (int i = 1; i <= 5; ++i) {
std::lock_guard<std::mutex> lock(mtx);
data_queue.push(i);
std::cout << "生产数据:" << i << std::endl;
// 通知一个消费者线程
cv.notify_one();
}
// 生产结束,修改标志并通知所有消费者
std::lock_guard<std::mutex> lock(mtx);
finished = true;
cv.notify_all();
}
// 消费者线程函数
void consumer(int id) {
while (true) {
std::unique_lock<std::mutex> lock(mtx);
// 等待条件:队列不为空或者生产已经结束
cv.wait(lock, []() { return !data_queue.empty() || finished; });
// 如果队列为空且生产结束,退出循环
if (data_queue.empty() && finished) {
break;
}
// 消费数据
int data = data_queue.front();
data_queue.pop();
std::cout << "消费者" << id << "消费数据:" << data << std::endl;
lock.unlock(); // 提前释放锁,减少锁持有时间
}
}
int main() {
std::thread prod(producer);
std::thread cons1(consumer, 1);
std::thread cons2(consumer, 2);
prod.join();
cons1.join();
cons2.join();
return 0;
}
信号量与条件变量的区别
虽然两者都能实现线程同步,但存在明显差异:
| 对比维度 | 信号量 | 条件变量 |
|---|---|---|
| 依赖组件 | 无需依赖其他同步工具,自身维护计数 | 必须配合互斥锁使用 |
| 计数逻辑 | 内置计数,可直接反映可用资源数量 | 无内置计数,需要额外维护共享条件和计数 |
| 适用场景 | 控制同时访问资源的线程数量、线程执行顺序控制 | 等待某个条件成立再继续执行,生产者消费者场景 |
| 虚假唤醒 | 不存在虚假唤醒问题 | 可能出现虚假唤醒,需要配合谓词判断条件 |
使用注意事项
使用信号量时需要注意,C++20之前的标准没有内置信号量,若使用旧版本标准可以通过操作系统提供的信号量接口或者自行封装,但要注意跨平台兼容性。使用条件变量时,等待操作一定要传入谓词判断条件,避免虚假唤醒导致程序逻辑错误,同时互斥锁的粒度要尽可能小,减少线程阻塞时间。另外,通知线程时,notify_one只唤醒一个等待线程,notify_all唤醒所有等待线程,要根据实际场景选择,避免不必要的线程唤醒带来的性能开销。