前言:在多线程编程中,线程同步是确保程序正确执行的关键。C++提供了多种同步机制,其中信号量(Semaphore)和条件变量(Condition Variable)是两种重要的工具。
目录
一、信号量
资源访问的 “守门员”
信号量,你可以把它想象成一个计数器,它的主要职责是控制对共享资源的访问数量。比如,我们有一个打印机房,里面只有 3 台打印机。那我们就要确保同一时间最多只能有 3 个线程去使用打印机,信号量就能完美地完成这个任务。
(一)信号量的核心操作
信号量主要有两个操作:wait
(等待)和 post
(发布)。
-
wait
操作 :当一个线程想要访问资源时,它会先进行wait
操作。如果信号量的计数值大于 0,说明还有空闲资源,计数值减 1,线程可以顺利访问资源;但如果计数值为 0,线程就会被阻塞,直到有其他线程释放资源。 -
post
操作 :当线程使用完资源后,会进行post
操作,将信号量的计数值加 1,这样如果有线程正在等待这个资源,就有可能被唤醒去访问资源。
(二)代码示例:模拟打印任务
#include <iostream>
#include <thread>
#include <semaphore>
#include <vector>
#include <chrono>
// 定义一个信号量,初始值为 3,表示有 3 台打印机
std::counting_semaphore<3> printerSemaphore(3);
void printDocument(const std::string& docName, int printCount) {
for (int i = 1; i <= printCount; ++i) {
printerSemaphore.acquire(); // 等待信号量,尝试获取打印机
std::cout << "正在打印文档 " << docName << " 的第 " << i << " 页..." << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(500)); // 模拟打印时间
std::cout << "文档 " << docName << " 的第 " << i << " 页打印完成!" << std::endl;
printerSemaphore.release(); // 释放信号量,打印完成释放打印机
}
}
int main() {
// 创建多个线程模拟多个打印任务
std::thread t1(printDocument, "论文", 3);
std::thread t2(printDocument, "报告", 2);
std::thread t3(printDocument, "作业", 4);
t1.join();
t2.join();
t3.join();
return 0;
}
代码运行结果及解释 :
程序运行后,输出如下:
正在打印文档 论文 的第 正在打印文档 作业 的第 正在打印文档 1报告 的第 1 页...
页...
1 页...
文档 报告 的第 文档 作业 的第 1 页打印完成!
文档 论文 的第 1 页打印完成!
1 页打印完成!
正在打印文档 作业 的第 2 页...
正在打印文档 报告 的第 2 页...
正在打印文档 论文 的第 2 页...
文档 作业 的第 文档 报告 的第 2 页打印完成!
2 页打印完成!
正在打印文档 作业 的第 文档 论文 的第 3 页...
2 页打印完成!
正在打印文档 论文 的第 3 页...
文档 作业 的第 3 页打印完成!
正在打印文档 作业 的第 4 页...
文档 论文 的第 3 页打印完成!
文档 作业 的第 4 页打印完成!
从结果可以看出,最多同时有 3 个线程在打印,符合我们 3 台打印机的设定。每个线程在打印完一页后释放信号量,其他等待的线程就能继续打印。
二、条件变量
线程通信的 “传话筒”
条件变量的作用是让线程之间可以相互通信,使线程能够在某个条件满足时等待,或者在条件满足时唤醒其他线程。它的使用通常要和互斥锁(mutex
)配合。
(一)条件变量的核心操作
-
wait
操作 :线程调用wait
方法后会进入等待状态,并且会释放之前锁定的互斥锁。直到被其他线程唤醒,线程才会重新获取互斥锁并继续执行。 -
notify_one
操作 :用于唤醒一个在条件变量上等待的线程。 -
notify_all
操作 :用于唤醒所有在条件变量上等待的线程。
(二)代码示例:生产者 - 消费者问题
#include <iostream>
#include <thread>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <chrono>
std::queue<int> productQueue; // 产品队列
std::mutex queueMutex; // 互斥锁
std::condition_variable cv; // 条件变量
const int maxQueueSize = 10; // 队列最大容量
bool productionDone = false; // 生产是否完成
void producer(int producerId) {
for (int i = 1; i <= 20; ++i) {
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟生产时间
std::unique_lock<std::mutex> lock(queueMutex);
// 如果队列已满,则等待
cv.wait(lock, [] { return productQueue.size() < maxQueueSize || productionDone; });
if (productionDone) {
return; // 如果生产完成且队列已满,直接退出
}
productQueue.push(i);
std::cout << "生产者 " << producerId << " 生产了产品 " << i << ",当前队列中共有 " << productQueue.size() << " 个产品" << std::endl;
cv.notify_all(); // 通知消费者有新产品
}
// 生产完成后,设置标志位并通知消费者
{
std::unique_lock<std::mutex> lock(queueMutex);
productionDone = true;
}
cv.notify_all(); // 最后通知消费者生产已经完成
}
void consumer(int consumerId) {
while (true) {
std::unique_lock<std::mutex> lock(queueMutex);
// 如果队列为空且生产未完成,则等待;如果队列为空且生产完成,则退出
cv.wait(lock, [] { return !productQueue.empty() || productionDone; });
if (!productQueue.empty()) {
int product = productQueue.front();
productQueue.pop();
std::cout << "消费者 " << consumerId << " 消费了产品 " << product << ",当前队列中共有 " << productQueue.size() << " 个产品" << std::endl;
cv.notify_all(); // 通知其他线程
} else if (productionDone) {
break; // 如果队列为空且生产完成,退出循环
}
}
}
int main() {
std::thread producers[3]; // 3 个生产者线程
std::thread consumers[5]; // 5 个消费者线程
// 启动生产者线程
for (int i = 0; i < 3; ++i) {
producers[i] = std::thread(producer, i + 1);
}
// 启动消费者线程
for (int i = 0; i < 5; ++i) {
consumers[i] = std::thread(consumer, i + 1);
}
// 等待所有生产者线程结束
for (auto& t : producers) {
t.join();
}
// 等待所有消费者线程结束
for (auto& t : consumers) {
t.join();
}
return 0;
}
代码运行结果及解释 :
程序运行后的输出:
生产者 1 生产了产品 1,当前队列中共有 1 个产品
消费者 1 消费了产品 1,当前队列中共有 0 个产品
生产者 3 生产了产品 1,当前队列中共有 1 个产品
消费者 1 消费了产品 1,当前队列中共有 0 个产品
生产者 2 生产了产品 1,当前队列中共有 1 个产品
消费者 1 消费了产品 1,当前队列中共有 0 个产品
生产者 1 生产了产品 2,当前队列中共有 1 个产品
消费者 1 消费了产品 2,当前队列中共有 0 个产品
生产者 2 生产了产品 2,当前队列中共有 1 个产品
消费者 4 消费了产品 2,当前队列中共有 0 个产品
生产者 3 生产了产品 2,当前队列中共有 1 个产品
消费者 5 消费了产品 2,当前队列中共有 0 个产品
生产者 1 生产了产品 3,当前队列中共有 1 个产品
消费者 3 消费了产品 3,当前队列中共有 0 个产品
生产者 3 生产了产品 3,当前队列中共有 1 个产品
消费者 3 消费了产品 3,当前队列中共有 0 个产品
生产者 2 生产了产品 3,当前队列中共有 1 个产品
消费者 3 消费了产品 3,当前队列中共有 0 个产品
生产者 1 生产了产品 4,当前队列中共有 1 个产品
消费者 3 消费了产品 4,当前队列中共有 0 个产品
生产者 3 生产了产品 4,当前队列中共有 1 个产品
消费者 5 消费了产品 4,当前队列中共有 0 个产品
生产者 2 生产了产品 4,当前队列中共有 1 个产品
消费者 5 消费了产品 4,当前队列中共有 0 个产品
生产者 1 生产了产品 5,当前队列中共有 1 个产品
消费者 4 消费了产品 5,当前队列中共有 0 个产品
生产者 2 生产了产品 5,当前队列中共有 1 个产品
消费者 2 消费了产品 5,当前队列中共有 0 个产品
生产者 3 生产了产品 5,当前队列中共有 1 个产品
消费者 3 消费了产品 5,当前队列中共有 0 个产品
生产者 1 生产了产品 6,当前队列中共有 1 个产品
消费者 3 消费了产品 6,当前队列中共有 0 个产品
生产者 2 生产了产品 6,当前队列中共有 1 个产品
生产者 3 生产了产品 6,当前队列中共有 2 个产品
消费者 3 消费了产品 6,当前队列中共有 1 个产品
消费者 3 消费了产品 6,当前队列中共有 0 个产品
生产者 1 生产了产品 7,当前队列中共有 1 个产品
消费者 5 消费了产品 7,当前队列中共有 0 个产品
生产者 3 生产了产品 7,当前队列中共有 1 个产品
消费者 5 消费了产品 7,当前队列中共有 0 个产品
生产者 2 生产了产品 7,当前队列中共有 1 个产品
消费者 4 消费了产品 7,当前队列中共有 0 个产品
生产者 1 生产了产品 8,当前队列中共有 1 个产品
生产者 3 生产了产品 8,当前队列中共有 2 个产品
消费者 5 消费了产品 8,当前队列中共有 1 个产品
消费者 2 消费了产品 8,当前队列中共有 0 个产品
生产者 2 生产了产品 8,当前队列中共有 1 个产品
消费者 5 消费了产品 8,当前队列中共有 0 个产品
生产者 1 生产了产品 9,当前队列中共有 1 个产品
消费者 4 消费了产品 9,当前队列中共有 0 个产品
生产者 3 生产了产品 9,当前队列中共有 1 个产品
消费者 3 消费了产品 9,当前队列中共有 0 个产品
生产者 2 生产了产品 9,当前队列中共有 1 个产品
消费者 1 消费了产品 9,当前队列中共有 0 个产品
生产者 3 生产了产品 10,当前队列中共有 1 个产品
消费者 1 消费了产品 10,当前队列中共有 0 个产品
生产者 2 生产了产品 10,当前队列中共有 1 个产品
消费者 2 消费了产品 10,当前队列中共有 0 个产品
生产者 1 生产了产品 10,当前队列中共有 1 个产品
消费者 3 消费了产品 10,当前队列中共有 0 个产品
生产者 3 生产了产品 11,当前队列中共有 1 个产品
消费者 3 消费了产品 11,当前队列中共有 0 个产品
生产者 1 生产了产品 11,当前队列中共有 1 个产品
消费者 1 消费了产品 11,当前队列中共有 0 个产品
生产者 2 生产了产品 11,当前队列中共有 1 个产品
消费者 4 消费了产品 11,当前队列中共有 0 个产品
生产者 3 生产了产品 12,当前队列中共有 1 个产品
消费者 1 消费了产品 12,当前队列中共有 0 个产品
生产者 1 生产了产品 12,当前队列中共有 1 个产品
消费者 1 消费了产品 12,当前队列中共有 0 个产品
生产者 2 生产了产品 12,当前队列中共有 1 个产品
消费者 1 消费了产品 12,当前队列中共有 0 个产品
生产者 3 生产了产品 13,当前队列中共有 1 个产品
消费者 2 消费了产品 13,当前队列中共有 0 个产品
生产者 1 生产了产品 13,当前队列中共有 1 个产品
消费者 3 消费了产品 13,当前队列中共有 0 个产品
生产者 2 生产了产品 13,当前队列中共有 1 个产品
消费者 2 消费了产品 13,当前队列中共有 0 个产品
生产者 1 生产了产品 14,当前队列中共有 1 个产品
生产者 3 生产了产品 14,当前队列中共有 2 个产品
生产者 2 生产了产品 14,当前队列中共有 3 个产品
消费者 3 消费了产品 14,当前队列中共有 2 个产品
消费者 3 消费了产品 14,当前队列中共有 1 个产品
消费者 3 消费了产品 14,当前队列中共有 0 个产品
生产者 1 生产了产品 15,当前队列中共有 1 个产品
消费者 4 消费了产品 15,当前队列中共有 0 个产品
生产者 2 生产了产品 15,当前队列中共有 1 个产品
消费者 3 消费了产品 15,当前队列中共有 0 个产品
生产者 3 生产了产品 15,当前队列中共有 1 个产品
消费者 3 消费了产品 15,当前队列中共有 0 个产品
生产者 1 生产了产品 16,当前队列中共有 1 个产品
消费者 3 消费了产品 16,当前队列中共有 0 个产品
生产者 2 生产了产品 16,当前队列中共有 1 个产品
消费者 2 消费了产品 16,当前队列中共有 0 个产品
生产者 3 生产了产品 16,当前队列中共有 1 个产品
消费者 2 消费了产品 16,当前队列中共有 0 个产品
生产者 1 生产了产品 17,当前队列中共有 1 个产品
消费者 1 消费了产品 17,当前队列中共有 0 个产品
生产者 2 生产了产品 17,当前队列中共有 1 个产品
消费者 1 消费了产品 17,当前队列中共有 0 个产品
生产者 3 生产了产品 17,当前队列中共有 1 个产品
消费者 1 消费了产品 17,当前队列中共有 0 个产品
生产者 2 生产了产品 18,当前队列中共有 1 个产品
消费者 1 消费了产品 18,当前队列中共有 0 个产品
生产者 3 生产了产品 18,当前队列中共有 1 个产品
消费者 1 消费了产品 18,当前队列中共有 0 个产品
生产者 1 生产了产品 18,当前队列中共有 1 个产品
消费者 1 消费了产品 18,当前队列中共有 0 个产品
生产者 2 生产了产品 19,当前队列中共有 1 个产品
消费者 2 消费了产品 19,当前队列中共有 0 个产品
生产者 1 生产了产品 19,当前队列中共有 1 个产品
消费者 1 消费了产品 19,当前队列中共有 0 个产品
生产者 3 生产了产品 19,当前队列中共有 1 个产品
消费者 1 消费了产品 19,当前队列中共有 0 个产品
生产者 2 生产了产品 20,当前队列中共有 1 个产品
消费者 1 消费了产品 20,当前队列中共有 0 个产品
在这个示例中,生产者线程生产产品并放入队列,当队列满时生产者会等待;消费者线程从队列中消费产品,当队列空时消费者会等待。当所有产品生产完成后,生产者会通知消费者生产结束,消费者在确认队列空且生产结束后退出。
三、信号量和条件变量的区别
特性 | 信号量 | 条件变量 |
---|---|---|
核心功能 | 控制对有限资源的访问数量 | 线程间通信,等待条件满足 |
使用场景 | 限制对一组资源的访问(如连接池) | 生产者-消费者模式,多阶段处理 |
操作方式 | acquire /release | wait /notify_one /notify_all |
线程阻塞原因 | 资源不足 | 条件不满足 |
互斥锁配合 | 内置互斥机制(如counting_semaphore) | 需要手动配合std::mutex使用 |
适用场景复杂度 | 简单资源控制 | 复杂线程间通信场景 |
总结
信号量和条件变量都是 C++ 中实现线程同步的重要工具。信号量主要关注对有限资源的访问控制,确保同一时间对资源的访问数量不超过限定值;而条件变量更侧重于线程之间的通信,让线程能够在特定条件下等待或唤醒其他线程。在实际开发中,要根据具体的应用场景选择合适的同步机制。掌握好这两个工具,你就能在多线程编程的世界里更加得心应手,编写出高效、可靠的并发程序。