一、互斥量的概念
作用场景:当一个代码里面有一个共享数据时,如果多个线程要使用这个共享数据,程序就有可能崩溃。因为对于一个共享数据来说,不可能一个线程在写数据的同时,另一个线程来读数据。因此要保护共享数据,用代码把共享数据锁住,当一个线程对共享数据读或写时,其他线程就必须等待。
互斥量(mutex)的基本概念:互斥量是个类对象,可以理解成一把锁。多个线程尝试用lock()成员函数来加锁,只有一个线程能锁定成功,锁定成功的标志是lock()函数返回。如果没锁成功,那么线程中的流程就会卡在lock()这里去不断的尝试加锁。
注意:互斥量在使用的过程中要慎重,加锁的范围不能多也不能少,多了会影响效率,少了可能会导致程序崩溃。
二、互斥量的用法
先添加头文件
#include<mutex>
对于要操作共享数据的线程,先lock()加锁,操作共享数据,最后unlock()释放。
lock()与unlock()要成对使用!
案例代码:案例内涵看上篇:C++11多线程数据共享_阿巴乾的博客-CSDN博客
//
// Created by yangjq on 22-7-7.
//
#include <iostream>
#include <thread>
#include <vector>
#include <list>
#include <mutex>
using namespace std;
class A{
public:
//把玩家命令收到一个队列的线程
void inMsgRecvQueue(){
for(int i = 0; i < 1000000000; ++i){
cout << "inMsgRecvQueue()执行,插入一个元素" << i << endl;
std::lock_guard<std::mutex> sbguard(my_mutex);
//my_mutex.lock();
msgRecvQueue.push_back(i);//数字i就是玩家收到的命令
//my_mutex.unlock();
}
}
bool outMsgLULProc(int &command){
std::lock_guard<std::mutex> sbguard(my_mutex);
//my_mutex.lock();
if(!msgRecvQueue.empty()) {
//消息不为空
int command = msgRecvQueue.front();//返回前面的元素
msgRecvQueue.pop_front();
//my_mutex.unlock();
//处理数据...
return true;
}
//my_mutex.unlock();//两个出口需要两个unlock();
return false;
}
//数据从消息队列中取出的线程
void outMsgRecvQueue(){
int command = 0;
for(int i = 0; i < 100000000; ++i){
bool result = outMsgLULProc(command);
if(result){
cout << "outMsgRecvQueue()执行,删除一个元素!" << endl;
}
else{
cout << "outMsgRecvQueue()执行,消息队列为空" << i << endl;
}
}
}
private:
//容器,专门用于存储玩家发送过来的命令
std::list<int> msgRecvQueue;
std::mutex my_mutex;
};
int main()
{
//用成员函数作为线程函数的方法来写线程
A myobja;
std::thread myOutMsgObj(&A::outMsgRecvQueue, &myobja);
std::thread myInMsgObj(&A::inMsgRecvQueue, &myobja);
myInMsgObj.join();
myOutMsgObj.join();
cout << "main end !" << endl;
return 0;
}
上面这份代码里面lock(),unlock()与lock_guard的加锁用法都有,前者被注释掉了。由于前者的用法需要配对,不配对就会报错,所以出现了lock_guard的用法。lock_guard是一个类模板,可以直接取代lock(),unlock()。
以上面代码中的lock_guard用法为例,解释以下lock_guard。在上面代码中
std::lock_guard<std::mutex> sbguard(my_mutex);
构造了一个对象,sbguard是随便起得对象名,在构造这个对象的过程中,执行了这个类的构造函数,这个构造函数中就有lock(),在退出函数时,会执行析构函数,析构函数中执行了unlock()。
因此,lock_guard只有在函数退出的时候才能解锁,为了提高效率,可以在局部加上{ },来提前进行析构函数提高效率。
三、死锁
死锁是由至少两个锁头也就是两个互斥量才能产生。在C++中,假设现在一段代码有两把锁,锁A与锁B。并且现在有两个线程,线程1与线程2。
- 线程1执行的时候,线程1先锁上了锁A,然后需要去锁上锁B
- 同时,线程2执行了,线程1先锁上了锁B,然后需要去锁上锁A
这时,死锁就产生了。两个线程都在等待已经被对方锁上的锁,都不能解锁,也都不能拿到对方的锁。
死锁演示代码:
//
// Created by yangjq on 22-7-7.
//
#include <iostream>
#include <thread>
#include <vector>
#include <list>
#include <mutex>
using namespace std;
class A{
public:
//把玩家命令收到一个队列的线程
void inMsgRecvQueue(){
for(int i = 0; i < 1000000000; ++i){
cout << "inMsgRecvQueue()执行,插入一个元素" << i << endl;
my_mutex1.lock();
my_mutex2.lock();
msgRecvQueue.push_back(i);//数字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();
//处理数据...
return true;
}
my_mutex1.unlock();
my_mutex2.unlock();//两个出口需要两个unlock();
return false;
}
//数据从消息队列中取出的线程
void outMsgRecvQueue(){
int command = 0;
for(int i = 0; i < 100000000; ++i){
bool result = outMsgLULProc(command);
if(result){
cout << "outMsgRecvQueue()执行,删除一个元素!" << endl;
}
else{
cout << "outMsgRecvQueue()执行,消息队列为空" << i << endl;
}
}
}
private:
//容器,专门用于存储玩家发送过来的命令
std::list<int> msgRecvQueue;
std::mutex my_mutex1;
std::mutex my_mutex2;
};
int main()
{
//用成员函数作为线程函数的方法来写线程
A myobja;
std::thread myOutMsgObj(&A::outMsgRecvQueue, &myobja);
std::thread myInMsgObj(&A::inMsgRecvQueue, &myobja);
myInMsgObj.join();
myOutMsgObj.join();
cout << "main end !" << endl;
return 0;
}
以上的代码运行时会卡住,那么死锁的问题怎么解决呢?
其实两个锁的顺序一致就不会死锁,都先锁1再锁2就不会发生死锁。
除此之外,可以利用std::lock(),使用方式是
std::lock(my_mutex1,my_mutex2)
这样就能同时对两个锁进行上锁,要么两个锁同时上锁,要么两个锁同时不锁。加入只有一个锁上锁成功,它会自动释放已上锁的锁。