1 先看一下以下示例代码,是不是存在一定风险呢?
#include <iostream>
#include <vector>
#include <thread>
#include <mutex>
#include <list>
using namespace std;
class M
{
public:
void InmessageQueue()
{
cout << "将要存数据,还没拿到锁,这个线程id是: " << std::this_thread::get_id() << endl;
for (int i = 0; i < 10000; i++)
{
std::unique_lock<std::mutex> sbguard(my_mutex);
cout << "插入一个元素。" << endl;
messageQueue.push_back(i);
}
}
void OutputMessage()
{
int command;
while (1)//注意,这里不会判断数据队列里是否为空,就是一味的取数据。
{
std::unique_lock<std::mutex> sguard(my_mutex);
cout << "正在取数据,这个线程id是: " << std::this_thread::get_id() << endl;
command = messageQueue.front();
messageQueue.pop_front();
}
cout << "end......." << endl;
}
private:
std::list<int> messageQueue;
std::mutex my_mutex;
};
int main()
{
// 一、条件变量std::condition_variable\wait()\notify_one()
//线程A: 等待一个条件满足
//线程B: 专门往消息队列中扔数据
M queue;
thread mytobj2(&M::OutputMessage, &queue);
thread mytobj1(&M::InmessageQueue, &queue);
mytobj1.join();
mytobj2.join();
return 0;
}
上述代码,我们看到取数据线程是不会判断数据队列中是否有数据的,并且我们启动线程时,取数据线程首先启动,这就很容易导致,取数据线程先拿到锁,当队列里没有数据时,程序就会崩溃。运行结果如下:
条件变量可以解决这个问题。
2 条件变量std::condition_variable\wait()\notify_one()
条件变量实现下列功能:
线程A: 等待一个条件满足
线程B: 专门往消息队列中扔数据
std::condition_variable是一个类,说白了就是等待一个条件达成。
得和互斥量配合工作
联想到上面那段代码,我们可以提出这样的解决方案,在取数据的线程中设置等待,要等到存数据线程给“已经插入数据”的信号以后再进行取数据。
那他怎么实现的呢?
①在类中声明一个条件变量对象:std::condition_variable mycond;
②在取数据线程设置等待mycond.wait(sguard);
③同时在存数据线程中设置唤醒mycond.notify_one();
private:
std::list<int> msgRecvQueue;// 容器,专门用于代表晚间给咱们发送过来的指令。
std::mutex my_mutex;// 创建一个互斥量(一把锁)
std::condition_variable mycond;// 条件变量对象,需要和互斥量配合使用
void Outmessage()
{
int command;
while (1)
{
std::unique_lock<std::mutex> sguard(my_mutex);
/*如果lamada表达式返回false,那就解锁互斥量并堵塞到本行,注意这块已经解锁了哈。
直到存数据线程调用了notify_one()。
mycond.wait(sguard),跟上面一样。*/
mycond.wait(sguard, [this] {
if (!msgRecvQueue.empty())
{
return true;
}
else
return false;
});
/*等被notify_one唤醒后,注意
a)wait不断尝试重新去拿锁,如果获取不到,仍会卡在此位置。(有可能存数据线程唤醒了他,但是存数据线程还拿着锁呢)
b)拿到锁,1)若wait有第二个参数,那么就判断第二个参数的bool值,又变为上面的问题了。
2)若没有第二个参数,那么就相当于第二个参数返回true了。
*/
cout << "正在取数据,这个线程id是: " << std::this_thread::get_id() << endl;
command = messageQueue.front();
messageQueue.pop_front();
sguard.unlock();//unique_lock的灵活性,可以随时解锁。
}
cout << "end......." << endl;
}
void InmessageQueue()
{
cout << "将要存数据,还没拿到锁,这个线程id是: " << std::this_thread::get_id() << endl;
for (int i = 0; i < 10000; i++)
{
std::unique_lock<std::mutex> sbguard(my_mutex);
cout << "插入一个元素。id 为 " << std::this_thread::get_id() << endl;
messageQueue.push_back(i);
mycond.notify_one();//尝试唤醒取数据线程
}
}
3 notify_all()
创建两个取数据的线程,要想在插数据的线程中把所有取数据线程唤醒,就用这个。了解了notify_one(),也就很容易理解notify_all(),就是尝试唤醒多个线程,比如,我们创建两个取数据线程,那我们就得在存数据线程中用notify_all()来唤醒其中一个线程(随机唤醒,由于互斥锁的存在仍是只有一个线程取数据)。
M queue;
thread myOutnMsgObj(&M::OutputMessage, &queue);//注意,第二个对象要用引用,保证子线程中用的就是主线程提供的对象。
thread myOutnMsgObj1(&M::OutputMessage, &queue);
thread myInMsgObj(&M::InmessageQueue, &queue);
myOutnMsgObj.join();
myOutnMsgObj1.join();
myInMsgObj.join();
void InmessageQueue()
{
for (int i = 0; i < 10000; i++)
{
cout << "inMsgRecvQueue()执行,插入一个元素 " << i << endl;
std::unique_lock<std::mutex> sbguard(my_mutex);//将unique_lock和mutex绑定,并lock()
messageQueue.push_back(i);// 把收到的消息,放进消息队列中。
mycond.notify_all();//换成notify_all()
}
}