「围观」C++11条件变量到底有多强?五分钟带你彻底搞懂线程同步!
看完这篇,保证你对C++条件变量有种"哦,原来如此!"的顿悟感。不信?往下看就知道了!
大家好啊,我是小康。今天咱们聊一个听起来挺高深,但其实超实用的话题 —— C++11条件变量。
说实话,我第一次接触这玩意儿时也是一脸懵逼:"条件变量?这不就是个变量吗,有啥好讲的?"
结果一看代码,顿时傻眼了...
但别慌!今天我用最白话的方式帮你彻底搞懂它。不讲那些晦涩的概念,就讲你真正需要知道的东西。
微信搜索 「跟着小康学编程」,关注我,后续还有更多硬核技术文章分享,带你玩转 Linux C/C++ 编程!😆
条件变量到底是个啥?
想象你和朋友在肯德基排队,但你突然想上厕所。
你对朋友说:"哥们,我去个卫生间,到咱们了你喊我一声啊!"
然后你去卫生间了,但并不是一直站在那儿傻等,而是该干嘛干嘛去了。
这就是条件变量的核心思想:一个线程(你)在等待某个条件满足(队排到了),另一个线程(你朋友)负责在条件满足时通知等待的线程(你)。
条件变量的厉害之处就是:它让等待的线程能够暂时"睡眠",不消耗CPU资源,直到被另一个线程唤醒。
为啥要用条件变量?
直接上一个生活中的例子:
假设你在煮方便面,说好了3分钟熟。你有两种等待方式:
- 傻等法:眼睛死盯着锅和手表,不停地问自己"好了没?好了没?"(这就是所谓的"忙等待",特别浪费资源)
- 聪明等法:设个3分钟闹钟,然后玩会手机,闹铃响了再去看锅(这就是条件变量的思想)
显然,聪明等法更高效,既不浪费你的注意力(CPU资源),事情也能圆满完成。
条件变量的基本用法
C++11中,我们主要用到这两个类:
- std::condition_variable- 就是我们的条件变量主角
- std::mutex- 它的好搭档,互斥锁
基本用法分2步:
1. 等待条件满足(等待方)
std::unique_lock<std::mutex> lock(mutex); // 先上锁
     
cv.wait(lock, [&]{ return 条件满足; });;   // 不满足就等待(自动释放锁并休眠)
}
// 条件满足了,继续执行
// 锁还在手里,记得用完放开
2. 满足条件并通知(通知方)
{
    std::lock_guard<std::mutex> lock(mutex); // 先上锁
    // 改变条件状态
    条件 = true;
} // 锁自动释放
cv.notify_one(); // 通知一个等待的线程
// 或
cv.notify_all(); // 通知所有等待的线程
就这么简单!
但是,光说不练假把式,来看个具体例子。
经典案例:生产者-消费者问题
我们用做早餐来解释:
- 生产者:就是做煎饼的师傅(不断地生产煎饼)
- 消费者:就是饥肠辘辘的食客(不断地吃煎饼)
- 共享缓冲区:就是放煎饼的托盘(容量有限)
规则很简单:
- 托盘满了,师傅就得等等(生产者等待)
- 托盘空了,食客就得等等(消费者等待)
- 师傅做好一个,告诉食客可以吃了(生产者通知)
- 食客吃完一个,告诉师傅可以继续做了(消费者通知)
代码实现:
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <chrono>
using namespace std;
// 共享数据及同步对象
queue<int> products;         // 煎饼托盘
mutex mtx;                   // 互斥锁
condition_variable cv_empty;  // 托盘空了的条件变量
condition_variable cv_full;   // 托盘满了的条件变量
const int MAX_PRODUCTS = 5;   // 托盘最多放5个煎饼
// 生产者线程(做煎饼的师傅)
void producer() {
    for (int i = 1; i <= 10; ++i) {  // 做10个煎饼
        {
            unique_lock<mutex> lock(mtx);  // 先上锁
            // 如果托盘满了,就等待
            cv_empty.wait(lock, []{
                return products.size() < MAX_PRODUCTS;
            });
            // 做一个煎饼,放到托盘上
            products.push(i);
            cout << "师傅做好第 " << i << " 个煎饼,托盘现在有 " 
                << products.size() << " 个煎饼\n";
        } // 解锁
        // 通知消费者有煎饼可以吃了
        cv_full.notify_one();
        // 做煎饼需要一点时间
        this_thread::sleep_for(chrono::milliseconds(300));
    }
}
// 消费者线程(吃煎饼的食客)
void consumer() {
    for (int i = 1; i <= 10; ++i) {  // 要吃10个煎饼
        int product;
        {
            unique_lock<mutex> lock(mtx);  // 先上锁
            // 如果托盘空了,就等待
            cv_full.wait(lock, []{
                return !products.empty();
            });
            // 从托盘取一个煎饼吃
            product = products.front();
            products.pop();
            cout << "食客吃掉第 " << product << " 个煎饼,托盘还剩 " 
                << products.size() << " 个煎饼\n";
        } // 解锁
        // 通知生产者托盘有空位了
        cv_empty.notify_one();
        // 吃煎饼需要一点时间
        this_thread::sleep_for(chrono::milliseconds(500));
    }
}
int main() {
    cout << "===== 煎饼店开张啦! =====\n";
    thread t1(producer);  // 启动生产者线程
    thread t2(consumer);  // 启动消费者线程
    t1.join();  // 等待生产者线程结束
    t2.join();  // 等待消费者线程结束
    cout << "===== 煎饼卖完了! =====\n";
    return 0;
}
运行结果可能是这样的:
===== 煎饼店开张啦! =====
师傅做好第 1 个煎饼,托盘现在有 1 个煎饼
食客吃掉第 1 个煎饼,托盘还剩 0 个煎饼
师傅做好第 2 个煎饼,托盘现在有 1 个煎饼
师傅做好第 3 个煎饼,托盘现在有 2 个煎饼
食客吃掉第 2 个煎饼,托盘还剩 1 个煎饼
师傅做好第 4 个煎饼,托盘现在有 2 个煎饼
食客吃掉第 3 个煎饼,托盘还剩 1 个煎饼
师傅做好第 5 个煎饼,托盘现在有 2 个煎饼
食客吃掉第 4 个煎饼,托盘还剩 1 个煎饼
师傅做好第 6 个煎饼,托盘现在有 2 个煎饼
食客吃掉第 5 个煎饼,托盘还剩 1 个煎饼
师傅做好第 7 个煎饼,托盘现在有 2 个煎饼
食客吃掉第 6 个煎饼,托盘还剩 1 个煎饼
师傅做好第 8 个煎饼,托盘现在有 2 个煎饼
食客吃掉第 7 个煎饼,托盘还剩 1 个煎饼
师傅做好第 9 个煎饼,托盘现在有 2 个煎饼
食客吃掉第 8 个煎饼,托盘还剩 1 个煎饼
师傅做好第 10 个煎饼,托盘现在有 2 个煎饼
食客吃掉第 9 个煎饼,托盘还剩 1 个煎饼
食客吃掉第 10 个煎饼,托盘还剩 0 个煎饼
===== 煎饼卖完了! =====
看到没?师傅和食客配合得多默契啊!这就是条件变量的魅力:让两个线程之间能够无缝协作。
条件变量的几个关键点
1. 为什么有时看到 while 循环检查条件?
也许你注意到了,示例代码用的是 lambda 函数而不是 while 循环。但在老式写法中,我们通常这样:
while (!条件满足) {
    cv.wait(lock);
}
不用 if 而用 while 的原因是:虚假唤醒。
有时候,等待的线程可能会在没有人通知的情况下醒来(就像你睡觉时突然被楼上装修吵醒)。如果用 if,线程会错误地认为条件已满足;用 while,它会再检查一遍,发现条件没满足,继续等待。
2. wait() 的两种用法
条件变量的 wait() 有两种调用方式:
// 方式1:只传递锁
cv.wait(lock);
// 方式2:传递锁和判断条件(推荐)
cv.wait(lock, []{ return 条件满足; });
方式 2 相当于:
while (!条件满足) {
    cv.wait(lock);
}
但方式 2 更简洁、更不容易出错,强烈推荐使用!
3. 重要的超时等待函数
有时候,我们不想无限期等待,而是最多等待一段时间。C++11提供了超时版本的 wait 函数:
// 最多等待100毫秒
auto status = cv.wait_for(lock, chrono::milliseconds(100), []{ return 条件满足; });
if (status) {
    // 条件满足了
} else {
    // 超时了,条件仍未满足
}
这就像你等外卖:如果 30 分钟送不到,我就自己做饭吃了!
微信搜索 「跟着小康学编程」,关注我,后续还有更多硬核技术文章分享,带你玩转 Linux C/C++ 编程!😆
高级案例:线程池中的任务调度
想象一个更复杂的例子:一个简单的线程池。这是很多高性能系统的基础设施:
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
#include <vector>
#include <functional>
using namespace std;
class ThreadPool {
private:
vector<thread> workers;          // 工作线程
queue<function<void()>> tasks;   // 任务队列
mutex mtx;                       // 互斥锁
condition_variable cv;           // 条件变量
bool stop;                       // 停止标志
public:
// 构造函数,创建指定数量的工作线程
ThreadPool(size_t threads) : stop(false) {
    for (size_t i = 0; i < threads; ++i) {
        workers.emplace_back([this] {
            while (true) {
                function<void()> task;
                {
                    unique_lock<mutex> lock(this->mtx);
                    // 等待任务或停止信号
                    this->cv.wait(lock, [this] {
                        return this->stop || !this->tasks.empty();
                    });
                    // 如果线程池停止且没有任务,则退出
                    if (this->stop && this->tasks.empty()) {
                        return;
                    }
                    // 获取一个任务
                    task = move(this->tasks.front());
                    this->tasks.pop();
                }
                // 执行任务
                task();
            }
        });
    }
}
// 添加新任务到线程池
template<class F>
void enqueue(F&& f) {
    {
        unique_lock<mutex> lock(mtx);
        // 不允许在线程池停止后添加任务
        if (stop) {
            throw runtime_error("ThreadPool已停止,无法添加任务");
        }
        tasks.emplace(forward<F>(f));
    }
    // 通知一个等待的线程有新任务
    cv.notify_one();
}
// 析构函数,停止所有线程
~ThreadPool() {
    {
        unique_lock<mutex> lock(mtx);
        stop = true;
    }
    // 通知所有等待的线程
    cv.notify_all();
    // 等待所有线程结束
    for (auto& worker : workers) {
        worker.join();
    }
}
};
// 测试线程池
int main() {
    // 创建4个工作线程的线程池
    ThreadPool pool(4);
    // 添加一些任务
    for (int i = 1; i <= 8; ++i) {
        pool.enqueue([i] {
            cout << "任务 " << i << " 开始执行,线程ID: " 
                << this_thread::get_id() << endl;
            // 模拟任务执行时间
            this_thread::sleep_for(chrono::seconds(1));
            cout << "任务 " << i << " 执行完成" << endl;
        });
    }
    // 主线程暂停一会儿,让工作线程有时间执行任务
    this_thread::sleep_for(chrono::seconds(10));
    cout << "主线程退出" << endl;
    return 0;
}
运行结果可能是这样的:
任务 1 开始执行,线程ID: 140271052129024
任务 2 开始执行,线程ID: 140271060521728
任务 3 开始执行,线程ID: 140271068914432
任务 4 开始执行,线程ID: 140271077307136
任务 1 执行完成
任务 5 开始执行,线程ID: 140271052129024
任务 2 执行完成
任务 6 开始执行,线程ID: 140271060521728
任务 3 执行完成
任务 7 开始执行,线程ID: 140271068914432
任务 4 执行完成
任务 8 开始执行,线程ID: 140271077307136
任务 5 执行完成
任务 6 执行完成
任务 7 执行完成
任务 8 执行完成
主线程退出
看!多个线程自动分配任务,互不干扰,效率杠杠的!
条件变量使用的注意事项
1、 永远和互斥锁一起使用:条件变量需要和互斥锁配合,否则会导致竞态条件
2、 检查唤醒原因:被唤醒不一定是因为条件满足,所以总是要检查条件(用while或wait的谓词版本)
3、 注意通知时机:通常先改变条件状态,再发出通知,且通知应在解锁后进行
4、 区分 notify_one 和 notify_all:
- notify_one(): 只唤醒一个等待的线程(适合一对一通知)
- notify_all(): 唤醒所有等待的线程(适合广播通知)
5、 防止丢失唤醒:如果通知在等待之前发出,那么可能会丢失,导致线程永远等待
总结:条件变量,让你的多线程程序更高效!
条件变量就像多线程世界里的"微信群通知":让线程之间能够高效协调工作,不必浪费CPU资源去傻等。
关键知识点回顾:
- 条件变量用于线程间的等待/通知机制
- 必须与互斥锁配合使用
- 使用 wait() 等待条件满足
- 使用 notify_one()/notify_all() 通知等待的线程
- 总是在循环中检查条件,防止假唤醒
掌握了条件变量,你的C++多线程技能就上了一个台阶!再也不用担心线程间如何优雅地协作啦~
写在最后:一起玩转C++多线程世界!
怎么样?条件变量是不是没那么可怕了?
如果这篇文章让你对 C++ 多线程有了新的认识,不妨点赞、收藏、关注。支持一下!你的每次点赞都是我熬夜码字的动力源泉!💪
有问题?有困惑?评论区随时欢迎你的声音!我看到必回,咱们一起讨论进步~
🔥 独家福利 🔥
关注我的公众号「跟着小康学编程」,解锁更多编程秘籍:
- 🧠 深入C++核心:指针、智能指针、内存模型...那些让人头秃的概念,我都给你拆得明明白白
- 🚀 性能优化实战:让你的代码飞起来,性能提升不是梦
- 🔍 源码探秘:STL容器背后的黑科技,跟我一起扒个底朝天
- 👔 面试通关锦囊:大厂面试常考点,提前备好不慌张
- 🛠️ 项目实战经验:实用技巧和避坑指南,让你少走弯路
每周持续更新,不定期还有学习路线图和常见后端硬核技术文章!
扫描下方二维码关注我,和我一起玩转C++,做更好的自己!
记得转发给你的程序猿/媛朋友,好东西就是要分享嘛~ 😉
一键解锁更多技术干货!
👇 戳这里!扫码关注,不迷路!

C++技术交流圈已开启:
如果单打独斗遇到困难,我创建了一个技术小圈子,专为 C++ 爱好者准备。代码卡住了?概念不清楚?随时在群里抛出问题,不仅有我,还有一群经验丰富的开发者一起帮你答疑解惑。毕竟,编程路上有同行者,进步才会更快。

 
                     
                    
                 
                    
                
 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号