一.互斥量的概念
保护共享数据,操作时,某个线程用代码把共享数据锁住、操作数据、解锁
其他线程想操作共享数据必须等待解锁。
互斥量:
是个类对象,理解成一把锁,多个线程尝试用lock()来加锁,只有一个线程会加锁成功,成功的标志就是lock()返回,如果不成功,会一直阻塞在lock()处。
互斥量使用要小心,保护数据不多也不能少,少了,没有达到保护的效果,多了,影响效率。
二.互斥量的用法
1.lock()、unlock()
步骤:先lock(),操作共享数据,unlock()
lock()与unlock()要成对使用,有lock()必然要有unlock(),每调用一次lock(),必然应该调用一次unlock();不应该也不允许调用一次lock(),却调用了2次unlock(),这些非对称数量的调用都会导致代码不稳定甚至崩溃。实例代码如下:
#include <bits/stdc++.h>
#include <thread>
#include <mutex>
using namespace std;
//成员函数作为线程的入口函数
class A{
public:
//把收到的玩家命令放到队列的一个线程 写
void inMsgRecvQueue(){
for(int i = 0; i < 100000; i++){ //模拟了100000次
cout<<"inMsgRecvQueue执行,插入一个元素:"<<i<<endl;
my_mutex.lock(); //保护共享数据
msgRecvQueue.push_back(i);
my_mutex.unlock();
}
}
bool outMsgLULProc(int &command)
{
my_mutex.lock();
if (!msgRecvQueue.empty())
{
//消息不为空
int command = msgRecvQueue.front();//返回第一个元素,但不检查元素是否存在
msgRecvQueue.pop_front();//移除第一个元素。但不返回;
my_mutex.unlock(); //所有分支都必须有unlock()
return true;
}
my_mutex.unlock();
return false;
}
//处理玩家命令的线程 读
void outMsgRecvQueue(){
int command = 0;
for(int i = 0; i < 100000; i++){ //模拟了100000次
bool result = outMsgLULProc(command);
if(result){
cout << "outMsgRecvQueue()执行,取出一个元素"<< endl;
//处理数据
}else{
cout<<"outMsgRecvQueue执行,消息队列为空"<<i<<endl;
}
}
cout<<"end"<<endl;
}
private:
list<int> msgRecvQueue; //玩家发送的命令。需要保护的共享数据
mutex my_mutex; //创建一个互斥量
};
int main(){
A obj;
thread myOut(&A::outMsgRecvQueue,ref(obj));//第二个参数是引用,才能保证线程里用的是同一个对象
thread myIn(&A::inMsgRecvQueue,ref(obj));//第二个参数是引用,才能保证线程里用的是同一个对象
myIn.join();
myOut.join();
return 0;
}
lock()之后,必须进行unlock()。
为了防止大家忘记unlock(),引入了一个叫std::lock_guard的类模板:忘记unlock(),替你unlock()。如同智能指针(unique_ptr<>),你忘记释放内存不要紧,我替你释放。
2.lock_guard类模板
直接取代lock()和unlock(),用了lock_guard之后,不能再使用lock()和unlock()。
生成lock_guard对象的时候,调用构造函数,构造函数中会执行lock(),当析构的时候,会执行unlock()。
#include <bits/stdc++.h>
#include <thread>
#include <mutex>
using namespace std;
//成员函数作为线程的入口函数
class A{
public:
//把收到的玩家命令放到队列的一个线程 写
void inMsgRecvQueue(){
for(int i = 0; i < 100000; i++){ //模拟了100000次
cout<<"inMsgRecvQueue执行,插入一个元素:"<<i<<endl;
//my_mutex.lock(); //保护共享数据
{
lock_guard<mutex> m(my_mutex);
msgRecvQueue.push_back(i);
} //提前结束lock_guard对象,析构调用unlock()
//my_mutex.unlock();
//其他代码
}
}
bool outMsgLULProc(int &command)
{
lock_guard<mutex> m(my_mutex);
//my_mutex.lock();
if (!msgRecvQueue.empty())
{
//消息不为空
int command = msgRecvQueue.front();//返回第一个元素,但不检查元素是否存在
msgRecvQueue.pop_front();//移除第一个元素。但不返回;
//my_mutex.unlock(); //所有分支都必须有unlock()
return true;
}
//my_mutex.unlock();
return false;
}
//处理玩家命令的线程 读
void outMsgRecvQueue(){
int command = 0;
for(int i = 0; i < 100000; i++){ //模拟了100000次
bool result = outMsgLULProc(command);
if(result){
cout << "outMsgRecvQueue()执行,取出一个元素"<< endl;
//处理数据
}else{
cout<<"outMsgRecvQueue执行,消息队列为空"<<i<<endl;
}
}
cout<<"end"<<endl;
}
private:
list<int> msgRecvQueue; //玩家发送的命令。需要保护的共享数据
mutex my_mutex; //创建一个互斥量
};
int main(){
A obj;
thread myOut(&A::outMsgRecvQueue,ref(obj));//第二个参数是引用,才能保证线程里用的是同一个对象
thread myIn(&A::inMsgRecvQueue,ref(obj));//第二个参数是引用,才能保证线程里用的是同一个对象
myIn.join();
myOut.join();
return 0;
}
三.死锁
产生死锁,至少有两个互斥量。
两个线程A、B;两把锁:金锁 ,银锁
(1)线程A执行的时候,这个线程先锁金锁,把金锁lock()成功了,然后它去lock银锁。
(2)此时出现了上下文切换
(3)线程B执行了,这个线程先锁银锁,因为银锁还没被锁,所以银锁会lock()成功,线程B要去lock()金锁。
(4)此时此刻,死锁就产生了;
(5)线程A因为拿不到银锁头,流程走不下去(所有后边代码有解金锁的但是流程走不下去,所以金锁解不开)
(6)线程B因为拿不到金锁头,流程走不下去(所有后边代码有解银锁的但是流程走不下去,所以银锁解不开)
大家都晾在那里,你等我,我等你
1.死锁的演示
以下代码会产生死锁,两个线程上锁的顺序正好是反着的,解锁顺序无所谓。
#include <bits/stdc++.h>
#include <thread>
#include <mutex>
using namespace std;
//成员函数作为线程的入口函数
class A{
public:
//把收到的玩家命令放到队列的一个线程 写
void inMsgRecvQueue(){
for(int i = 0; i < 100000; i++){ //模拟了100000次
cout<<"inMsgRecvQueue执行,插入一个元素:"<<i<<endl;
my_mutex1.lock(); //保护共享数据
my_mutex2.lock(); //保护共享数据
msgRecvQueue.push_back(i);
my_mutex2.unlock();
my_mutex1.unlock(); //保护共享数据
}
}
bool outMsgLULProc(int &command)
{
my_mutex2.lock();
my_mutex1.lock();
if (!msgRecvQueue.empty())
{
//消息不为空
int command = msgRecvQueue.front();//返回第一个元素,但不检查元素是否存在
msgRecvQueue.pop_front();//移除第一个元素。但不返回;
my_mutex1.unlock();
my_mutex2.unlock(); //所有分支都必须有unlock()
return true;
}
my_mutex1.unlock();
my_mutex2.unlock();
return false;
}
//处理玩家命令的线程 读
void outMsgRecvQueue(){
int command = 0;
for(int i = 0; i < 100000; i++){ //模拟了100000次
bool result = outMsgLULProc(command);
if(result){
cout << "outMsgRecvQueue()执行,取出一个元素"<< endl;
//处理数据
}else{
cout<<"outMsgRecvQueue执行,消息队列为空"<<i<<endl;
}
}
cout<<"end"<<endl;
}
private:
list<int> msgRecvQueue; //玩家发送的命令。需要保护的共享数据
mutex my_mutex1; //创建一个互斥量
mutex my_mutex2; //创建一个互斥量
};
int main(){
A obj;
thread myOut(&A::outMsgRecvQueue,ref(obj));//第二个参数是引用,才能保证线程里用的是同一个对象
thread myIn(&A::inMsgRecvQueue,ref(obj));//第二个参数是引用,才能保证线程里用的是同一个对象
myIn.join();
myOut.join();
return 0;
}
2.死锁的一般解决方案
- 保证两个互斥量的加锁顺序是一致的
3.lock()函数模板
用来处理多个互斥量的时候才出场。
一次锁住两个或者两个以上的互斥量(至少两个),不存在因为在多个线程中,因为加锁的顺序的问题而导致的死锁问题。---->一次性申请到所需要的全部资源,破坏死锁的"保持和请求"条件。
std::lock():如果互斥量中有一个没有锁住,它就在那等着,等所有互斥量都锁住,它才能往下走(返回);要么两个互斥量都锁柱,要么两个互斥量都没锁住,如果只锁了一个,另外一个没有锁成功,则它立即把已经锁住的解锁。
还是需要手动进行unlock()。
实例代码如下:
#include <bits/stdc++.h>
#include <thread>
#include <mutex>
using namespace std;
//成员函数作为线程的入口函数
class A{
public:
//把收到的玩家命令放到队列的一个线程 写
void inMsgRecvQueue(){
for(int i = 0; i < 100000; i++){ //模拟了100000次
cout<<"inMsgRecvQueue执行,插入一个元素:"<<i<<endl;
lock(my_mutex1,my_mutex2);
msgRecvQueue.push_back(i);
my_mutex2.unlock();
my_mutex1.unlock(); //保护共享数据
}
}
bool outMsgLULProc(int &command)
{
lock(my_mutex1,my_mutex2);
if (!msgRecvQueue.empty())
{
//消息不为空
int command = msgRecvQueue.front();//返回第一个元素,但不检查元素是否存在
msgRecvQueue.pop_front();//移除第一个元素。但不返回;
my_mutex1.unlock();
my_mutex2.unlock(); //所有分支都必须有unlock()
return true;
}
my_mutex1.unlock();
my_mutex2.unlock();
return false;
}
//处理玩家命令的线程 读
void outMsgRecvQueue(){
int command = 0;
for(int i = 0; i < 100000; i++){ //模拟了100000次
bool result = outMsgLULProc(command);
if(result){
cout << "outMsgRecvQueue()执行,取出一个元素"<< endl;
//处理数据
}else{
cout<<"outMsgRecvQueue执行,消息队列为空"<<i<<endl;
}
}
cout<<"end"<<endl;
}
private:
list<int> msgRecvQueue; //玩家发送的命令。需要保护的共享数据
mutex my_mutex1; //创建一个互斥量
mutex my_mutex2; //创建一个互斥量
};
int main(){
A obj;
thread myOut(&A::outMsgRecvQueue,ref(obj));//第二个参数是引用,才能保证线程里用的是同一个对象
thread myIn(&A::inMsgRecvQueue,ref(obj));//第二个参数是引用,才能保证线程里用的是同一个对象
myIn.join();
myOut.join();
return 0;
}
4.lock_guard的adopt_lock参数
adopt_lock是个结构体对象,起一个标志作用:就是表示这个互斥量已经lock(),不需要在lock_guardstd::mutex构造函数里再对mutex对象进行再次lock()了。实例代码如下:
#include <bits/stdc++.h>
#include <thread>
#include <mutex>
using namespace std;
//成员函数作为线程的入口函数
class A{
public:
//把收到的玩家命令放到队列的一个线程 写
void inMsgRecvQueue(){
for(int i = 0; i < 100000; i++){ //模拟了100000次
cout<<"inMsgRecvQueue执行,插入一个元素:"<<i<<endl;
lock(my_mutex1,my_mutex2);
lock_guard<mutex> m1(my_mutex1,adopt_lock);
lock_guard<mutex> m2(my_mutex2,adopt_lock);
msgRecvQueue.push_back(i);
}
}
bool outMsgLULProc(int &command)
{
lock(my_mutex1,my_mutex2);
lock_guard<mutex> m1(my_mutex1,adopt_lock);
lock_guard<mutex> m2(my_mutex2,adopt_lock);
if (!msgRecvQueue.empty())
{
//消息不为空
int command = msgRecvQueue.front();//返回第一个元素,但不检查元素是否存在
msgRecvQueue.pop_front();//移除第一个元素。但不返回;
//my_mutex1.unlock();
//my_mutex2.unlock(); //所有分支都必须有unlock()
return true;
}
// my_mutex1.unlock();
//my_mutex2.unlock();
return false;
}
//处理玩家命令的线程 读
void outMsgRecvQueue(){
int command = 0;
for(int i = 0; i < 100000; i++){ //模拟了100000次
bool result = outMsgLULProc(command);
if(result){
cout << "outMsgRecvQueue()执行,取出一个元素"<< endl;
//处理数据
}else{
cout<<"outMsgRecvQueue执行,消息队列为空"<<i<<endl;
}
}
cout<<"end"<<endl;
}
private:
list<int> msgRecvQueue; //玩家发送的命令。需要保护的共享数据
mutex my_mutex1; //创建一个互斥量
mutex my_mutex2; //创建一个互斥量
};
int main(){
A obj;
thread myOut(&A::outMsgRecvQueue,ref(obj));//第二个参数是引用,才能保证线程里用的是同一个对象
thread myIn(&A::inMsgRecvQueue,ref(obj));//第二个参数是引用,才能保证线程里用的是同一个对象
myIn.join();
myOut.join();
return 0;
}
总结:lock()一次锁定多个互斥量;谨慎使用(建议一个一个锁),因为不同的互斥量控制不同的共享数据,两个互斥量在一起的情况不多见。