【多线程】1.条件变量--std::condition_variable

本文详细探讨了C++中的条件变量如何实现实时线程同步,包括条件变量的基本使用、虚假唤醒的概念及解决方法,通过实例展示了条件变量在工作流中的应用,如生产者-消费者模型。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

条件变量允许我们通过通知进而实现线程同步
因此,您可以实现发送方/接收方或生产者/消费者之类的工作流。
在这样的工作流程中,接收者正在等待发送者的通知。如果接收者收到通知,它将继续工作。


1. std::condition_variable

条件变量可以履行发送者或接收者的角色。
作为发送者,它可以通知一个或多个接收者。
这就是使用条件变量所需要知道的基本所有内容,程序示例:

// conditionVariable.cpp

#include <iostream>
#include <condition_variable>
#include <mutex>
#include <thread>

std::mutex mutex_;
std::condition_variable condVar;

void doTheWork(){
  std::cout << "Processing shared data." << std::endl;
}

void waitingForWork(){
    std::cout << "Worker: Waiting for work." << std::endl;

    std::unique_lock<std::mutex> lck(mutex_);
    condVar.wait(lck);
    doTheWork();
    std::cout << "Work done." << std::endl;
}

void setDataReady(){
    std::cout << "Sender: Data is ready."  << std::endl;
    condVar.notify_one();
}

int main(){

  std::cout << std::endl;

  std::thread t1(waitingForWork);
  std::thread t2(setDataReady);

  t1.join();
  t2.join();

  std::cout << std::endl;
  
}

该程序有两个子线程: t1t2
它们在第33行和第34行中获得可调用的有效负载(函数或函子) waitingForWorksetDataReady
函数setDataReady通过使用条件变量condVar调用condVar.notify_one()进行通知。
在持有锁的同时,线程t1正在等待通知: condVar.wait(lck).

在等待的线程会执行的步骤:

在等待的线程总是会执行相同的步骤:线程醒来 -> 试图得到锁 -> 检查是否持有锁:

  • 如果通知到达,并在获取锁失败的情况下,让自己回到睡眠状态;
  • 在获取锁成功的情况下,线程离开上面的线程醒来 -> 试图得到锁 -> 检查是否持有锁循环过程并继续其工作。


该程序的输出也没什么意外
这里写图片描述


但是那是我的第一印象,一个例子有限的测试次数说明不了问题。接下来再看虚假的唤醒

2. 虚假的唤醒

细节决定成败。事实上,可能发生的是,接收方发送方发出通知之前完成了任务。
接收方虚假唤醒,然后执行完wait()后的内容,而后发送方才发出通知。虚假唤醒,使得发送方通知没了意义,甚至可能出现隐患。


这怎么可能呢?

接收方虚假的唤醒很敏感。所以即使没有通知发生接收方也有可能会醒来。
为了保护它,我不得不向等待方法添加一个判断。


这就是我在下一个例子中所做的(存在缺陷:可能唤醒不了):

// conditionVariableFixed.cpp

#include <iostream>
#include <condition_variable>
#include <mutex>
#include <thread>

std::mutex mutex_;
std::condition_variable condVar;

bool dataReady;

void doTheWork(){
  std::cout << "Processing shared data." << std::endl;
}

void waitingForWork(){
    std::cout << "Worker: Waiting for work." << std::endl;

    std::unique_lock<std::mutex> lck(mutex_);
    condVar.wait(lck,[]{return dataReady;});
    doTheWork();
    std::cout << "Work done." << std::endl;
}

void setDataReady(){
    std::lock_guard<std::mutex> lck(mutex_);
    dataReady=true;
    std::cout << "Sender: Data is ready."  << std::endl;
    condVar.notify_one();
}

int main(){

  std::cout << std::endl;

  std::thread t1(waitingForWork);
  std::thread t2(setDataReady);

  t1.join();
  t2.join();

  std::cout << std::endl;
  
}

与第一个示例的关键区别是在第11行中使用了一个布尔变量dataReady 作为附加条件。
dataReady在第28行中被设置为true

它在函数waitingForWork中被检查:

condVar.wait(lck,[]{return dataReady;})

这就是为什么wait方法有一个额外的重载,它接受一个判定。判定是个callable,它返回truefalse
在此示例中,callablelambda函数。因此,条件变量检查两个条件:判定是否为真,通知是否发生。

关于dataReady:

dataReady是个共享变量,将会被改变。所以我不得不用锁来保护它。
因为线程t2只设置和释放锁一次,所以std::lock_guard已经够用了。但是线程t1就不行了,wait方法将持续锁定和解锁互斥体(原因点击此处,参考“在等待的线程会执行的步骤”)。
所以我需要更强大的锁:std::unique_lock
但这还不是全部,条件变量有很多挑战,它们必须用锁来保护,并且易受虚假唤醒的影响。
大多数用例都很容易用tasks来解决,后续再说task问题。


  • 虚假唤醒: 接收方发送方发出通知之前完成了任务, 即,线程t1先结束wait,唤醒线程t1完成任务,然后线程t2才发送通知。
  • 唤醒不了: 发送方接收方进入等待wait状态之前发送通知,则通知会丢失。即,先通知,后等待。

3. 唤醒不了

条件变量的异常行为还是有的。大约每10次执行一次conditionVariable.cpp就会发生一些奇怪的现象:

为什么线程t2明明notify_one通知了,线程t1却还一直在wait阻塞等待?
这里写图片描述

我开始不知道怎么回事,这种现象完全违背了我对条件变量的直觉。
在安东尼·威廉姆斯的支持下,我解开了谜团。


问题在于:
如果发送方接收方进入等待状态之前发送通知,则通知会丢失。所以需要线程t1wait等待的语句先执行到wait位置,线程t2的notify_one语句后执行,才能唤醒t1的wait。C ++标准同时也将条件变量描述为同步机制,condition_variable类是一个同步原语,可以用来同时阻塞一个线程或多个线程…”

因此,通知消息已经丢失了,但是接收方还在等啊和等啊等啊等啊…

怎么解决这个问题呢?
答:去除掉wait第二个参数的判定可以有效帮助唤醒。实际上,在判定设置为真的情况下,接收器也能够独立于发送者的通知进而继续其工作。


译者水平有限,大多谷歌翻译,看不懂的请看原文地址。对于部分人,我又不赚你什么钱,原文也已经附上了,你在喷什么shit?你自己贡献了什么?给对你有用的博文点过赞?

原文地址:http://www.modernescpp.com/index.php/condition-variables

### 如何使用 `std::condition_variable` 和 `std::unique_lock<std::mutex>` 实现线程同步 以下是基于 `std::condition_variable` 和 `std::unique_lock<std::mutex>` 的线程同步示例代码: ```cpp #include <iostream> #include <thread> #include <mutex> #include <condition_variable> // 定义互斥锁和条件变量 std::mutex mtx; std::condition_variable cond_var; bool data_ready = false; // 数据准备状态标志 void worker_thread(int id) { std::unique_lock<std::mutex> lock(mtx); // 使用 unique_lock 加锁 while (!data_ready) { // 循环等待数据准备好 cond_var.wait(lock); // 等待条件变量的通知 } std::cout << "Thread " << id << ": Data processed\n"; // 执行任务 } void producer() { std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟生产时间延迟 { std::lock_guard<std::mutex> lock(mtx); // 使用 lock_guard 自动加解锁 data_ready = true; // 修改共享资源的状态 } cond_var.notify_all(); // 通知所有等待的线程 } int main() { std::vector<std::thread> threads; // 启动多个工作线程 for (int i = 0; i < 5; ++i) { threads.emplace_back(worker_thread, i); } // 生产者线程设置数据就绪状态并通知其他线程 std::thread prod(producer); // 等待所有线程完成 for (auto& th : threads) { th.join(); } prod.join(); return 0; } ``` #### 解析 - **互斥锁 (`std::mutex`) 和独占锁 (`std::unique_lock<std::mutex>`)** - `std::mutex` 提供基本的互斥功能,用于保护共享资源[^1]。 - `std::unique_lock<std::mutex>` 是一种更灵活的锁管理器,支持超时锁定、递归锁定等功能,并且可以直接与 `std::condition_variable` 配合使用[^2]。 - **条件变量 (`std::condition_variable`)** - 条件变量允许线程在某个特定条件下进入休眠状态,直到另一个线程发出通知为止[^3]。 - 在本例中,消费者线程会调用 `cond_var.wait(lock)` 方法释放锁并进入睡眠状态,直到生产者线程通过 `notify_all()` 唤醒它们[^4]。 - **线程间的通信逻辑** - 工作线程(消费者)会在循环中检查全局变量 `data_ready` 是否为真。如果未满足条件,则继续等待条件变量的通知。 - 当生产者线程修改了共享资源并将 `data_ready` 设为真后,它会调用 `notify_all()` 或 `notify_one()` 唤醒等待中的线程[^5]。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值