一、Condition Variable(条件变量)的意图
- 在前文的文章中(详情见“八”:https://blog.csdn.net/qq_41453285/article/details/105602105),我们有一个演示案例,让某线程等待另一线程,其使用的办法是使用ready flag的方法。代码如下:
-
bool readyFlag;
-
std::mutex readyFlagMutex;
-
-
void thread1()
-
{
-
//做一些thread2需要的准备工作
-
//...
-
std::lock_guard<
std::mutex> lg(readyFlagMutex);
-
readyFlag =
true;
-
}
-
-
void thread2()
-
{
-
//等待readyFlag变为true
-
{
-
std::unique_lock<
std::mutex> ul(readyFlagMutex);
-
//如果readyFlag仍未false,说明thread1还没有锁定,那么持续等待
-
while (!readyFlag)
-
{
-
ul.unlock();
-
std::this_thread::yield();
-
std:this_thread::sleep_for(
std::chrono::milliseconds(
100));
-
ul.lock();
-
}
-
}
//释放lock
-
-
//在thread1锁定之后,做相应的事情
-
}
- 但是上面的代码效率很低,原因有:
- 标准库提供了条件变量,它是个变量,借助它,一个线程可以唤醒一或多个其他等待中的线程
- 想要使用条件变量,需要配合mutex、unique_lock使用
- 因为条件变量也是一种变量,可能会有多个线程对其访问,因此在使用条件变量前,我们需要锁定一个mutex,然后这样才可以做好并发操作
- 在notify()端:当条件满足时,我们可以调用notify_one()或notify_all()通知wait()等待端条件满足
- 在wait()端:因为有多个线程需要对条件变量进行访问,因此我们先锁定mutex,此处用的是unique_lock<>锁住muex。然后进行一系列的操作,最终调用wait()等待在条件变量上,等待的时候其他线程可能还需要使用mutex,因此在wait()的内部会调用unique_lock的unlock()函数,在等待的过程中将mutex进行解锁,这样wait()的时候才不会因为mutex永久阻塞而造成其他线程无法继续向前执行
- 但是不能使用lock_guard,因为等待中的函数有可能锁定或解除mutex,但是lock_guard不提供lock()或unlock()函数,unique_lock()提供lock()或unlock()函数
- “假醒”的注意事项:
- “假醒”就是某个条件变量的wait()动作有可能在该条件变量尚未被notified时便返回
- 以下引自Anthony Williams的[Williams:CondVar]:“假醒无法被测定,以使用者的观点来看它们实质上是随机的。然而它们通常发生于thread library无法可靠确定某个waiting thread不遗漏任何notification时。由于遗漏notification便代表条件变量无用,thread library宁愿在线程的wait之中唤醒它而不愿承受风险”
- 因此,发生wakeup不一定意味着线程所需要的条件已经掌握了。更确切地说,在wakeup之后你仍然需要代码去验证“条件实际已达成”。因此(例如)我们必须检查数据是否真正备妥,或是我们仍需要诸如ready flag之类的东西。为了假设和查询其他端供应的数据或ready flag,可使用同一个mutex
二、演示案例
-
#include <iostream>
-
#include <thread>
-
#include <future>
-
#include <chrono>
-
#include <condition_variable>
-
using
namespace
std;
-
-
bool readyFlag;
-
std::mutex readyMutex;
-
std::condition_variable readyCondVar;
-
-
void thread1()
-
{
-
std::
cout <<
"thread1 starting..." <<
std::
endl;
-
std::
cout <<
"cin:" <<
std::
endl;
-
std::
cin.get();
-
-
{
-
//锁住条件变量,然后将readyFlag变为true
-
std::lock_guard<
std::mutex> lg(readyMutex);
-
readyFlag =
true;
-
}
//作用域结束之后,条件变量自动释放
-
-
//通知其他阻塞在readyCondVar条件变量上的代码
-
readyCondVar.notify_one();
-
std::
cout <<
"thread1 done" <<
std::
endl;
-
}
-
-
void thread2()
-
{
-
std::
cout <<
"thread2 starting..." <<
std::
endl;
-
-
{
-
std::unique_lock<
std::mutex> ul(readyMutex);
-
//等待在readyCondVar条件变量上(参数2见下面介绍)
-
readyCondVar.wait(ul, [] {
return readyFlag; });
-
}
-
-
std::
cout <<
"thread2 done" <<
std::
endl;
-
}
-
-
int main()
-
{
-
auto f1 =
std::async(
std::launch::async, thread1);
-
auto f2 =
std::async(
std::launch::async, thread2);
-
-
this_thread::sleep_for(
std::chrono::minutes(
1));
-
}
运行结果
- 我们随机运行一次的结果如下所述:
- 当我们随便输入一个字符之后,线程1先结束,然后thread1()中的条件变量通知thread2()中的wait(),使其返回,然后thread2()也跟着结束
代码解释
- 在thread1()中(通知者):
- 我们锁住readyMutex,然后更新readyFlag,之后当作用域结束之后,readyMutex自动被释放
- 然后通知条件变量readyCondVar
- 在thread2()中(等待者):
- 先是用一个unique_lock锁住mutex,然后等待wait条件变量readyCondVar
- 注意,此处不能使用lock_guard,而必须使用unique_lock,因为wait()内部会明确地对mutex进行解锁和加锁
- 注意此处wait()有两个参数:第一个参数为unique_lock,第二个参数是一个lambda,用来二次检测条件释放真的满足了,wait()在返回时内部会调用该lambda,然后判断readyFlag的值为true还是false,如果是true才真正的苏醒,否则继续等待
- wait()有两个重载版本,这个代码中使用的是含有两个参数的版本;还有一个版本只有一个参数
- 因此thread2()的wait()旁边的代码相当于下面的样子:
三、使用条件变量实现多线程Queue
-
#include <iostream>
-
#include <thread>
-
#include <future>
-
#include <chrono>
-
#include <queue>
-
#include <condition_variable>
-
using
namespace
std;
-
-
std::
queue<
int> _queue;
-
std::mutex queueMutex;
-
std::condition_variable queueCondVar;
-
-
void provider(int val)
-
{
-
for (
int i =
0; i <
6; ++i)
-
{
-
{
-
//锁定mutex,然后向_queue中加入元素
-
std::lock_guard<
std::mutex> lg(queueMutex);
-
_queue.push(val + i);
-
}
-
//通知等待在条件变量上的线程
-
queueCondVar.notify_one();
-
//延迟一会儿
-
std::this_thread::sleep_for(
std::chrono::milliseconds(val));
-
}
-
}
-
-
void consumer(int num)
-
{
-
while (
true)
-
{
-
int val;
-
{
-
//先锁住互斥量,然后对条件变量进行操作
-
std::unique_lock<
std::mutex> ul(queueMutex);
-
//等待条件变量通知,如果收到通知,并且_queue不为空才真正苏醒返回
-
queueCondVar.wait(ul, [] {
return !_queue.empty(); });
-
val = _queue.front();
-
_queue.pop();
-
}
-
std::
cout <<
"consumer: " << num <<
": " << val <<
std::
endl;
-
}
-
}
-
-
int main()
-
{
-
auto p1 =
std::async(
std::launch::async, provider,
100);
-
auto p2 =
std::async(
std::launch::async, provider,
300);
-
auto p3 =
std::async(
std::launch::async, provider,
500);
-
-
auto c1 =
std::async(
std::launch::async, consumer,
1);
-
auto c2 =
std::async(
std::launch::async, consumer,
2);
-
-
std::this_thread::sleep_for(
std::chrono::minutes(
1));
-
}
四、细说条件变量
- 头文件<condition_variable>针对条件变量提供了两个class,分别为condition_variable和condition_variable_any
condition_variable
- condition_variable用来唤醒一个或多个等待在某特定条件上的线程下面列出了condition_variable提供的操作:
- 所有等待(wait)某个条件的线程都必须使用相同的mutex,且必须使用unique_lock绑定mutex,并且让wait()等待在unique_lock上,否则会发生不明确的行为
- 如果无法创建条件变量,构造函数会抛出std::system_error异常并带有差错码resource_unavailable_try_again(相当于POSIX中 errno中的EAGAIN)
- 条件变量不提供复制和赋值操作
- wait_...()有两种重载形式:
- 一种是没有pred参数的:当收到条件变量通知时,直接返回
- 一种是带pred参数的:当收到条件变量通知时,还需要判断pred条件为true才真正苏醒。这种重载形式是为了防止“假醒”
- wait_for()和wait_until()的返回值:
- 不提供pred参数的wait_for()和wait_until()的返回值属于以下枚举类:
- std::cv_statue::timeout——在指定等待指定的时间之后,还没有收到通知,计时器超时
- std::cv_statue::no_timeout——在等待过程中收到了通知
- 提供pred参数的wait_for()和wait_until()的返回值为pred判断式的执行结果
- notify_all_at_thread_exit(cv,ul)全局函数:
- 用来在其调用者(线程)退出(exit)时调用notify_all()。为此它暂时锁住对应的lock l,后者必须使用所有等待线程共享的一个mutex
- 为避免死锁,线程调用notify_all_at_thread_exit()之后应该直接退出(exit)。因此这个调用只是为了在通知waiting thread之前先完成清理工作,而且这个清理工作绝不该造成阻塞
condition_variable_any
- condition_variable_any提供的操作与condition_variable类似(见上图),但是不提供native_handle()和notify_all_at_thread_exit()
- condition_variable_any不要求使用std::unique_lock对象当做lock
- 正如C++标准库所言:如果你使用的lock不是标准mutex类型,或者如果你使用标准mutex类型的一个群unique_lock wrapper并搭配condition_variable_any,那么使用者必须确保实现condition_variable_any实例对象所关联之predicate(判断式)的任何必要同步化
- 事实上该实例对象必须履行所谓的BasicLockable规定,该规定要求提供同步化的lock()和unlock()函数
转载:https://blog.csdn.net/qq_41453285/article/details/105605310
查看评论