C++11 condition_variable条件变量详解与实战
本文深入讲解C++11标准库中的condition_variable条件变量及其与std::mutex的协同工作机制。通过分析wait等待、notify通知机制及典型的多线程生产者-消费者场景代码,阐明如何利用共享变量状态控制线程阻塞与唤醒,解决多线程环境下的资源同步问题。
条件变量的基本概念与核心术语
在多线程编程中,condition_variable是一个同步原语类,主要用于防止多个线程对共享变量的访问发生冲突或混乱。它允许一个或多个线程挂起,直到另一个线程修改了共享变量并发出信号为止。
理解和使用条件变量需要把握以下三个核心概念:
- 互斥锁(Lock):用于锁定共享资源,确保同一时间只有一个线程能独占并修改该变量。
- 等待(Wait):线程在检查到条件不满足时进入阻塞状态,释放持有的锁,并等待外界通知。
- 通知(Notify):当共享变量发生变化且满足其他线程的预期条件时,发出信号唤醒正在等待的线程。
wait函数的两种使用方式
condition_variable提供了重载后的wait函数,以适应不同的业务逻辑需求:
// 基础版:无条件等待,直到被 notify 唤醒
void wait(std::unique_lock<std::mutex>& lock);
// 谓词版:不仅要求被唤醒,还要求满足特定的逻辑条件(通常是 Lambda 表达式)
template<class Predicate>
void wait(std::unique_lock<std::mutex>& lock, Predicate pred);
推荐使用带有 Predicate 的第二种方式,其等价逻辑如下:
while (!pred()) {
wait(lock);
}这种写法的优势在于能够处理“虚假唤醒”(Spurious Wakeups),即即使没有收到明确的 notify 信号,线程也可能被系统唤醒。通过循环检查谓词,可确保只有在条件真正满足时才继续执行后续代码。
多线程协作的标准操作流程
使用条件变量通常涉及两个角色:修改资源的“生产者”和等待资源的“消费者”。它们的交互流程严格遵循锁定、等待/修改、通知的顺序。
第一步:资源修改线程(生产者)的操作
- 获取锁:使用
std::unique_lock<std::mutex>锁定保护共享变量的互斥量。 - 修改变量:在持有锁的状态下,安全地修改共享变量的状态。
- 发送通知:调用
cv.notify_one()或cv.notify_all()唤醒等待的线程。注意:notify必须在解锁后或析构时进行效果最佳,以避免唤醒后立即因未解锁而再次阻塞。
第二步:资源等待线程(消费者)的操作
- 获取锁并检查条件:同样使用
std::unique_lock获取锁,并在循环中检查条件变量。 - 条件不满足则阻塞:若条件未达成,调用
wait。wait会自动原子性地释放锁并将线程挂起,让出 CPU 使用权。 - 被唤醒后重新获取锁:当收到
notify、超时或因虚假唤醒醒来时,wait会原子性地重新获取互斥锁。此时必须再次检查条件(配合谓词使用),确认无误后方可继续处理数据。
实战代码解析
场景一:无需 notify 的场景演示
在某些情况下,主线程可能在子线程启动前已经设置了条件。当子线程进入 wait 检查谓词时,发现条件已然成立,此时程序不会阻塞,而是直接向下执行。这展示了 wait 仅作为条件判断守卫的作用(即便没有触发 notify)。
#include <iostream>
#include <string>
#include <thread>
#include <mutex>
#include <condition_variable>
using namespace std;
std::mutex m;
std::condition_variable cv;
std::string data;
bool ready = false;
bool processed = false;
void worker_thread()
{
std::cout << "3、worker_thread子线程开始执行" << endl;
// Wait until main() sends data
std::unique_lock<std::mutex> lk(m);
std::cout << "4、worker_thread子线程获取到锁,条件满足无需notify,不阻塞向下执行" << endl;
// 此处 ready 已被主线程设为 true,所以不会阻塞
cv.wait(lk, []{return ready;});
// after the wait, we own the lock.
data += " after processing";
processed = true;
std::cout << "5、Worker thread signals data processing completed\n";
lk.unlock();
std::cout << "6、worker_thread子线程交出执行权限,主线程执行" << endl;
std::this_thread::sleep_for(std::chrono::milliseconds(2000));
cv.notify_one();
std::cout << "9、worker_thread调用 notify_one" << endl;
}
int main()
{
std::thread worker(worker_thread);
std::cout << "1、主线程开始执行" << std::endl;
data = "Example data";
{
std::lock_guard<std::mutex> lk(m);
ready = true; // 设置条件为真
}
std::cout << "2、锁已经释放了,主线程休眠,子线程执行" << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
{
std::cout << "7、主线程data:" << data << endl;
std::unique_lock<std::mutex> lk(m);
std::cout << "8、主线程条件满足无需notify" << endl;
cv.wait(lk, []{return processed;}); // 等待子线程处理完成
}
worker.join();
std::cout << "10、主线程结束" << endl;
}
执行结果示意:
场景二:正常的 notify 通知交互流程
这是更常见的情况:主线程先启动子线程,子线程发现条件不满足进入阻塞;随后主线程修改变量并发送 notify 唤醒子线程。由于 CPU 调度周期的存在,发出通知后并不保证被通知线程能立即获得执行权。
#include <iostream>
#include <string>
#include <thread>
#include <mutex>
#include <condition_variable>
using namespace std;
std::mutex m;
std::condition_variable cv;
std::string data;
bool ready = false;
bool processed = false;
void worker_thread()
{
std::cout << "3、worker_thread子线程开始执行" << endl;
std::unique_lock<std::mutex> lk(m);
std::cout << "4、worker_thread子线程获取到锁,条件不满足,释放lk锁,子线程阻塞" << endl;
// 等待主线程将 ready 设为 true
cv.wait(lk, []{return ready;});
std::cout << "8、worker_thread子线程获取到锁,子线程继续执行" << endl;
data += " after processing";
processed = true;
std::cout << "9、Worker thread signals data processing completed\n";
lk.unlock(); // 先解锁
std::this_thread::sleep_for(std::chrono::milliseconds(5000));
std::cout << "10、worker_thread调用 notify_one通知主线程执行" << endl;
cv.notify_one();
}
int main()
{
std::thread worker(worker_thread);
std::cout << "1、主线程开始执行" << std::endl;
data = "Example data";
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
{
std::lock_guard<std::mutex> lk(m);
ready = true;
}
cv.notify_one();
{
std::unique_lock<std::mutex> lk(m);
cv.wait(lk, []{return processed;});
}
worker.join();
std::cout << "11、主线程结束" << endl;
}
需要注意的是,notify_one 执行后,如果当前处于主线程的执行时钟周期内,主线程可能仍会继续运行一段逻辑,然后再由操作系统调度切换回被唤醒的子线程。因此,输出顺序可能出现交错。
场景三:验证执行时钟周期的影响
为了验证上述关于 CPU 调度的推测,我们在主线程中增加一段耗时操作(死循环),人为延长主线程的执行时间。这将导致即使子线程已准备好,也需等待主线程主动出让锁后才会继续。
// ... (头文件与其他全局变量定义同上)
int main()
{
std::thread worker(worker_thread);
std::cout << "1、主线程开始执行" << std::endl;
data = "Example data";
// 模拟耗时操作使子线程有机会进入等待
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
// 设置条件并通知
{
std::lock_guard<std::mutex> lk(m);
ready = true;
}
cv.notify_one();
// 主线程故意延迟,制造长执行周期
for(int i = 0; i < 10000000; i++) {
int j = i; // 空转
}
// 此时再尝试获取锁等待处理结果
{
std::cout << "7、由于主线程的执行时钟周期未结束,继续执行主线程获取lk, wait检查条件不满足,释放锁" << endl;
std::unique_lock<std::mutex> lk(m);
cv.wait(lk, []{return processed;});
}
worker.join();
std::cout << "11、主线程结束" << endl;
}
在该场景的输出日志中,可以观察到主线程在完成大量计算前,持续占用执行流,直观展示了条件变量调度与锁竞争的底层行为。
总结
正确运用 condition_variable 的核心在于:始终在持有锁的环境下进行条件检查,优先使用带谓词的 wait 方法以规避虚假唤醒风险,并注意 unlock 与 notify 的时序逻辑。以上就是C++11 condition_variable条件变量的详细内容,更多关于C++多线程同步的资料请关注本站其它相关文章!
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









