C++11引入的std::condition_variable是多线程编程中极为重要的同步原语,专门用来协调多个线程之间的等待和通知机制。它解决了线程间“等待某个条件成立再继续执行”的问题,配合互斥锁(std::mutex)使用,能高效且安全地实现线程间通信。
本文首发于【讳疾忌医-note】公众号,未经授权,不得转载。
个人教程网站内容更丰富:(www.1217zy.vip/)
1. 设计哲学:让线程“睡觉等待”,避免忙等浪费资源
在多线程环境中,线程经常需要等待某个条件(比如数据准备好、任务队列非空)才能继续执行。传统做法是“忙等”(busy-wait),即不断循环检查条件,这样不仅浪费CPU资源,还影响系统性能。
std::condition_variable的设计哲学是:
- • 线程阻塞等待:线程进入等待状态,不占用CPU资源,直到条件满足被唤醒。
- • 与互斥锁协作:保证条件检查和等待的原子性,避免竞态条件。
- • 支持多线程通知:可以唤醒一个线程(
notify_one)或全部等待线程(notify_all)。 - • 防止虚假唤醒:通过条件判断和循环等待,确保线程被唤醒时条件真正成立。
这让线程间的同步更高效、更安全,避免了资源浪费和复杂的手动同步。
2. std::condition_variable的基础用法
2.1 典型代码结构
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void worker(int id) {
std::unique_lock<std::mutex> lock(mtx);
// 等待条件成立,防止虚假唤醒用循环判断
cv.wait(lock, []{ return ready; });
std::cout << "Worker " << id << " is running\n";
}
void signal() {
{
std::lock_guard<std::mutex> lock(mtx);
ready = true; // 设置条件
}
cv.notify_all(); // 通知所有等待线程
}
int main() {
std::thread threads[5];
for (int i = 0; i < 5; ++i)
threads[i] = std::thread(worker, i);
std::cout << "Main thread preparing work...\n";
std::this_thread::sleep_for(std::chrono::seconds(1));
signal();
for (auto& t : threads)
t.join();
return 0;
}
解析:
- •
worker线程先获得unique_lock,调用cv.wait(lock, predicate)进入等待状态,自动释放锁,阻塞线程。 - • 当
signal函数调用notify_all后,所有等待线程被唤醒,重新获取锁,检查条件ready,满足后继续执行。 - •
wait的第二个参数是条件谓词,防止“虚假唤醒”(即线程被唤醒但条件未满足),保证线程安全。
3. 底层机制与细节
- •
std::condition_variable封装了操作系统的条件变量机制(如Linux的pthread_cond_t,Windows的条件变量API)。 - •
wait()函数会先释放互斥锁,使其他线程能进入临界区修改条件,然后阻塞等待通知。 - • 被通知后,线程会重新尝试获取锁,继续执行。
- • 虚假唤醒是多核系统和操作系统调度机制导致的正常现象,必须用循环判断条件避免错误执行。
- •
notify_one()唤醒一个等待线程,notify_all()唤醒所有等待线程,使用时根据业务需求选择。
4. 深度案例解析:生产者-消费者模型
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
std::mutex mtx;
std::condition_variable cv;
std::queue<int> dataQueue;
bool finished = false;
void producer() {
for (int i = 0; i < 10; ++i) {
{
std::lock_guard<std::mutex> lock(mtx);
dataQueue.push(i);
std::cout << "Produced: " << i << std::endl;
}
cv.notify_one(); // 通知消费者
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
{
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 !dataQueue.empty() || finished; });
if (!dataQueue.empty()) {
int value = dataQueue.front();
dataQueue.pop();
lock.unlock();
std::cout << "Consumer " << id << " consumed: " << value << std::endl;
} else if (finished) {
break;
}
}
}
int main() {
std::thread prod(producer);
std::thread cons1(consumer, 1);
std::thread cons2(consumer, 2);
prod.join();
cons1.join();
cons2.join();
return 0;
}
解析:
- • 生产者线程生产数据,放入队列后调用
notify_one唤醒一个消费者。 - • 消费者线程等待条件变量,条件是队列非空或生产结束。
- • 使用条件谓词避免虚假唤醒,保证线程安全。
- • 生产结束后,设置
finished标志并调用notify_all,唤醒所有消费者线程退出循环。
5. 进阶用法
- •
wait_for和wait_until:支持带超时的等待,避免死等。 - •
std::condition_variable_any:支持任意类型的锁,不仅限于std::unique_lock<std::mutex>。 - • 结合
std::atomic优化条件判断:减少锁竞争。 - • 避免锁内长时间阻塞:释放锁后等待,提升并发性能。
6. 常见错误及后果
- • 未使用循环判断条件:导致虚假唤醒时线程错误执行,产生竞态。
- • 通知前未持有锁或条件未设置好:可能导致“丢失通知”,线程永远等待。
- • 使用
notify_one唤醒多个线程场景:可能导致线程饥饿或死锁。 - • 条件变量与互斥锁不匹配:程序行为未定义。
- • 超时等待未正确处理:导致逻辑错误或资源浪费。
7. 大项目中使用注意事项
- • 严格遵守条件变量使用规范,确保条件判断和等待原子性。
- • 合理设计锁粒度和条件变量数量,避免性能瓶颈。
- • 结合日志和监控排查死锁和等待问题。
- • 使用带谓词的
wait函数,减少虚假唤醒影响。 - • 避免在持锁期间执行耗时操作,防止阻塞其他线程。
- • 结合线程池和任务队列设计高效并发模型。
8. 总结与独到见解
std::condition_variable是现代C++多线程同步的核心利器,它让线程间的等待和通知变得高效且安全。它的设计哲学体现了“让线程睡觉等待,避免忙等浪费资源”,通过与互斥锁紧密配合,保证了条件检查与等待的原子性。
我认为,条件变量的真正价值不仅在于“阻塞和唤醒”,更在于它是多线程协调的“桥梁”,让复杂的线程交互变得可控和可维护。掌握它的正确使用,尤其是条件谓词和循环等待,是写出健壮并发程序的关键。
在大型项目中,合理设计条件变量的使用场景,结合现代C++的其他并发特性,如std::atomic、线程池和任务系统,能打造出既高效又安全的并发架构。
(加入我的知识星球,免费获取账号,解锁所有文章。)