条件变量允许我们通过通知
进而实现线程同步
。
因此,您可以实现发送方/接收方或生产者/消费者之类的工作流。
在这样的工作流程中,接收者正在等待发送者的通知。如果接收者收到通知,它将继续工作。
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;
}
该程序有两个子线程: t1
和t2
。
它们在第33行和第34行中获得可调用的有效负载(函数或函子) waitingForWork
和setDataReady
。
函数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
,它返回true
或false
。
在此示例中,callable
是lambda函数
。因此,条件变量检查两个条件:判定是否为真,通知是否发生。
关于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
阻塞等待?
我开始不知道怎么回事,这种现象完全违背了我对条件变量的直觉。
在安东尼·威廉姆斯的支持下,我解开了谜团。
问题在于:
如果发送方
在接收方
进入等待状态之前发送通知,则通知会丢失。所以需要线程t1
的 wait
等待的语句先执行到wait
位置,线程t2的notify_one
语句后执行,才能唤醒t1的wait
。C ++标准同时也将条件变量描述为同步机制,condition_variable类
是一个同步原语,可以用来同时阻塞一个线程或多个线程…”
因此,通知消息已经丢失了,但是接收方还在等啊和等啊等啊等啊…
怎么解决这个问题呢?
答:去除掉wait
第二个参数的判定可以有效帮助唤醒。实际上,在判定设置为真的情况下,接收器也能够独立于发送者的通知进而继续其工作。
译者水平有限,大多谷歌翻译,看不懂的请看原文地址。对于部分人,我又不赚你什么钱,原文也已经附上了,你在喷什么shit?你自己贡献了什么?给对你有用的博文点过赞?
原文地址:http://www.modernescpp.com/index.php/condition-variables