目录
一、互斥量(mutex)的基本概念
1)保护共享数据,操作时,用代码把共享数据锁住,其他想操作共享数据的线程必须等待解锁。其他想操作共享数据的线程必须等待解锁,锁定,操作,解锁。引入互斥量(mutex)。
2)mutex就是一个类对象。理解成一把锁,多个线程尝试用lock()成员函数来加锁这把锁头,只有一个线程能锁成功(成功的标志是loc()函数返回),如果没有锁成功,那么流程就会卡在lock()这里不断的尝试去锁这把锁头。
3)互斥量使用要小心,保护数据不多也不少,少了达不到效果,多了影响效率。
二、互斥量的用法
2.1 lock(),unlock()
使用#include <mutex>
头文件
可以再private里创建一个互斥量
- 步骤:1.lock(),2.操作共享数据,3.unlock()。
- 使用规则:lock()和unlock()要成对使用,有lock必然有unlock,要对称数量的使用。每调用一次lock(),必然调用unlock()一次。
#include <iostream>
#include <thread>
#include <vector>
#include <list>
#include <string>
#include <mutex>
using namespace std;
class A {
public:
//把收到的信息(玩家命令) 排到一个队列的线程
void inMsgRecvQueue() {
for (int i = 0; i < 100000; ++i) {
cout << "inMsgRecvQueue()执行,插入一个元素" << endl;
my_mutex.lock();
msgRecvQueue.push_back(i); //假设这个数字i就是我收到的命令,我直接弄到消息队列里边
my_mutex.unlock();
}
return;
}
bool outMsgLULProc(int &command) {
//消息不为空
my_mutex.lock();
if (!msgRecvQueue.empty()) { //空不空也算读
//消息不为空
command = msgRecvQueue.front();//返回第一个元素,但是不检查元素是否存在
msgRecvQueue.pop_front(); //移除第一个元素但不返回
my_mutex.unlock();
return true;
}
my_mutex.unlock();
return false;
}
//把数据从消息队列中取出的线程
void outMsgRecvQueue() {
int command = 0;
for (int i = 0; i < 100000; ++i) {
bool result = outMsgLULProc(command);
if (result == true) {
cout << "outMsgRecvQueue()执行,取出一个元素" << command << endl;
//可以考虑进行命令(数据)的处理
}
else {
cout << "outMsgRecvQueue()执行,但目前队列消息队列为空" << i << endl;
}
}
cout << "end" << endl;
}
private:
std::list<int> msgRecvQueue; //容器,专门用来代表玩家给咱们发过来的命令。这里就相当于是共享内存
std::mutex my_mutex;//创建一个互斥量
};
int main() {
//准备用成员函数作为线程函数的方法来写线程;
A mytobj;
std::thread myOutMsgobj(&A::outMsgRecvQueue, &mytobj); //第二个参数是引用,这样就不是复制一份了,但是就不能用detach了
std::thread myInMsgobj(&A::inMsgRecvQueue, &mytobj);
myOutMsgobj.join();
myInMsgobj.join();//两个谁先谁后无所谓
return 0;
}
要保护的是共享数据!!!
bool outMsgLULProc(int &command) {
//消息不为空
my_mutex.lock();
if (!msgRecvQueue.empty()) { //空不空也算读
//消息不为空
command = msgRecvQueue.front();//返回第一个元素,但是不检查元素是否存在
msgRecvQueue.pop_front(); //移除第一个元素但不返回
my_mutex.unlock();
return true;
}
my_mutex.unlock();
return false;
}
注意这里面那个lock,unlock为什么不是成对出现,这是两个不同逻辑的成对。
2.2 std::lock_guard类模板
为了防止大家忘记unlock,引入了一个std::lock_guard的类模块,你忘记unlock不要紧,我替你unlock。就类似于智能指针里面忘记释放内存,自动帮你释放。
作用:直接取代lock()和unlock();也就是说,你用了lock_guard之后,再不能使用lock()和unlock();相当于构造函数执行了lock,析构执行了unlock
(整体代码,着重看18、28行)
#include <iostream>
#include <thread>
#include <vector>
#include <list>
#include <string>
#include <mutex>
using namespace std;
class A {
public:
//把收到的信息(玩家命令) 排到一个队列的线程
void inMsgRecvQueue() {
for (int i = 0; i < 100000; ++i) {
cout << "inMsgRecvQueue()执行,插入一个元素" << endl;
{
std::lock_guard<std::mutex> sbguard(my_mutex);
msgRecvQueue.push_back(i); //假设这个数字i就是我收到的命令,我直接弄到消息队列里边
}
}
return;
}
bool outMsgLULProc(int &command) {
//消息不为空
std::lock_guard<std::mutex> sbguard(my_mutex); //sbguard名字随便起的,构造函数执行了lock,析构执行了unlock
if (!msgRecvQueue.empty()) { //空不空也算读
//消息不为空
command = msgRecvQueue.front();//返回第一个元素,但是不检查元素是否存在
msgRecvQueue.pop_front(); //移除第一个元素但不返回
return true;
}
return false;
}
//把数据从消息队列中取出的线程
void outMsgRecvQueue() {
int command = 0;
for (int i = 0; i < 100000; ++i) {
bool result = outMsgLULProc(command);
if (result == true) {
cout << "outMsgRecvQueue()执行,取出一个元素" << command << endl;
//可以考虑进行命令(数据)的处理
}
else {
cout << "outMsgRecvQueue()执行,但目前队列消息队列为空" << i << endl;
}
}
cout << "end" << endl;
}
private:
std::list<int> msgRecvQueue; //容器,专门用来代表玩家给咱们发过来的命令。这里就相当于是共享内存
std::mutex my_mutex;//创建一个互斥量
};
int main() {
//准备用成员函数作为线程函数的方法来写线程;
A mytobj;
std::thread myOutMsgobj(&A::outMsgRecvQueue, &mytobj); //第二个参数是引用,这样就不是复制一份了,但是就不能用detach了
std::thread myInMsgobj(&A::inMsgRecvQueue, &mytobj);
myOutMsgobj.join();
myInMsgobj.join();//两个谁先谁后无所谓
return 0;
}
解释一个细节,加大括号:
1-(原来)
2-(用了lock_guard后,因为lock_guard是自动的,所以可以加个大括号限制)
三、死锁
3.1 死锁演示
死锁至少有两个互斥量mutex1,mutex2。
1)线程A执行时,这个线程先锁mutex1,并且锁成功了,然后去锁mutex2的时候,出现了上下文切换。
2)线程B执行,这个线程先锁mutex2,因为mutex2没有被锁,即mutex2可以被锁成功,然后线程B要去锁mutex1.
3)此时,死锁产生了,A锁着mutex1,需要锁mutex2,B锁着mutex2,需要锁mutex1,两个线程没办法继续运行下去。。。
#include <iostream>
#include <thread>
#include <vector>
#include <list>
#include <string>
#include <mutex>
using namespace std;
class A {
public:
//把收到的信息(玩家命令) 排到一个队列的线程
void inMsgRecvQueue() {
for (int i = 0; i < 100000; ++i) {
cout << "inMsgRecvQueue()执行,插入一个元素" << endl;
my_mutex1.lock(); //这两个lock不一定挨着,可能保护不同的数据共享块
my_mutex2.lock();
msgRecvQueue.push_back(i); //假设这个数字i就是我收到的命令,我直接弄到消息队列里边
my_mutex2.unlock();
my_mutex1.unlock();
}
return;
}
bool outMsgLULProc(int &command) {
//消息不为空
my_mutex2.lock();
my_mutex1.lock();
if (!msgRecvQueue.empty()) { //空不空也算读
//消息不为空
command = msgRecvQueue.front();//返回第一个元素,但是不检查元素是否存在
msgRecvQueue.pop_front(); //移除第一个元素但不返回
my_mutex1.unlock();
my_mutex2.unlock();
return true;
}
my_mutex1.unlock();
my_mutex2.unlock();
return false;
}
//把数据从消息队列中取出的线程
void outMsgRecvQueue() {
int command = 0;
for (int i = 0; i < 100000; ++i) {
bool result = outMsgLULProc(command);
if (result == true) {
cout << "outMsgRecvQueue()执行,取出一个元素" << command << endl;
//可以考虑进行命令(数据)的处理
}
else {
cout << "outMsgRecvQueue()执行,但目前队列消息队列为空" << i << endl;
}
}
cout << "end" << endl;
}
private:
std::list<int> msgRecvQueue; //容器,专门用来代表玩家给咱们发过来的命令。这里就相当于是共享内存
std::mutex my_mutex1;//创建一个互斥量
std::mutex my_mutex2;
};
int main() {
//准备用成员函数作为线程函数的方法来写线程;
A mytobj;
std::thread myOutMsgobj(&A::outMsgRecvQueue, &mytobj); //第二个参数是引用,这样就不是复制一份了,但是就不能用detach了
std::thread myInMsgobj(&A::inMsgRecvQueue, &mytobj);
myOutMsgobj.join();
myInMsgobj.join();//两个谁先谁后无所谓
return 0;
}
3.2 死锁的一般解决方案
只要保证多个互斥量上锁的顺序一样就不会造成死锁。
上一个案例出现死锁的原因就是:第一个先1锁,再2锁;第二个先2锁,再1锁。
改成第二个先1锁,再2锁就行
3.3 std::lock()函数模板
作用:一次锁住两个或者两个以上的互斥量(至少两个!!!一个就别用);它不存在这种因为多线程中因为顺序问题出现死锁的问题。
写法:==std::lock(mutex1,mutex2……);==后面解锁,unlock也得解锁n次。
1)std::lock():如果互斥量中有一个没锁住,就在那里等着,等所有互斥量都锁着,才能往下走(返回)
2)如果有一个没锁住,就会把已经锁住的释放掉(要么互斥量都锁住,要么都没锁住,防止死锁)
3.4 std::lock_guard的std::adopt_lock参数
1)加上std::adopt_lock可以不再调用lock功能
2)一定要紧挨着lock之后!!!
std::lock(my_mutex1,my_mutex2);
//std::lock_guard的std::adopt_lock要紧挨着lock之后
std::lock_guard<std::mutex> sbguard1(my_mutex1,std::adop_lock);
std::lock_guard<std::mutex> sbguard2(my_mutex2,std::adop_lock);
整体代码(注意31-34行!!):
#include <iostream>
#include <thread>
#include <vector>
#include <list>
#include <string>
#include <mutex>
using namespace std;
class A {
public:
//把收到的信息(玩家命令) 排到一个队列的线程
void inMsgRecvQueue() {
for (int i = 0; i < 100000; ++i) {
cout << "inMsgRecvQueue()执行,插入一个元素" << endl;
std::lock(my_mutex1,my_mutex2);
msgRecvQueue.push_back(i); //假设这个数字i就是我收到的命令,我直接弄到消息队列里边
my_mutex2.unlock();
my_mutex1.unlock();
}
return;
}
bool outMsgLULProc(int &command) {
//消息不为空
std::lock(my_mutex1, my_mutex2);
std::lock_guard<std::mutex> s1(my_mutex1, std::adopt_lock);
std::lock_guard<std::mutex> s2(my_mutex2, std::adopt_lock);
if (!msgRecvQueue.empty()) { //空不空也算读
//消息不为空
command = msgRecvQueue.front();//返回第一个元素,但是不检查元素是否存在
msgRecvQueue.pop_front(); //移除第一个元素但不返回
return true;
}
return false;
}
//把数据从消息队列中取出的线程
void outMsgRecvQueue() {
int command = 0;
for (int i = 0; i < 100000; ++i) {
bool result = outMsgLULProc(command);
if (result == true) {
cout << "outMsgRecvQueue()执行,取出一个元素" << command << endl;
//可以考虑进行命令(数据)的处理
}
else {
cout << "outMsgRecvQueue()执行,但目前队列消息队列为空" << i << endl;
}
}
cout << "end" << endl;
}
private:
std::list<int> msgRecvQueue; //容器,专门用来代表玩家给咱们发过来的命令。这里就相当于是共享内存
std::mutex my_mutex1;//创建一个互斥量
std::mutex my_mutex2;
};
int main() {
//准备用成员函数作为线程函数的方法来写线程;
A mytobj;
std::thread myOutMsgobj(&A::outMsgRecvQueue, &mytobj); //第二个参数是引用,这样就不是复制一份了,但是就不能用detach了
std::thread myInMsgobj(&A::inMsgRecvQueue, &mytobj);
myOutMsgobj.join();
myInMsgobj.join();//两个谁先谁后无所谓
return 0;
}