互斥量
通过上一篇文章,我们已经知道了怎么创建一个线程。现在我们要解决一个问题,要是我们创建了两个线程,它们都操作同一个对象,它会导致两个线程同时对这个对象进行修改,我们希望我们在运行其中一个线程的时候,另一个线程被阻塞住,等到这个线程执行完,再让另一个线程再来执行,循环往复。
这就要用到一个东西:互斥量。互斥量是一个类对象,可以理解成一把进程锁,多个线程尝试使用 lock() 来给它加锁,只有一个线程能够成功(成功标志是它返回了), 失败了则陷入忙等待,不断尝试加锁,以便可以访问核心资源。
互斥量的使用
接下来我们就用代码来演示互斥量的用法。
#include <iostream>
#include <queue>
#include <thread>
#include <mutex> //互斥量相关的头文件
using namespace std;
class A{
public:
//把消息插入消息队列的线程入口函数
void inMsgRecQue(){
for(int i = 0;i < 1000;++i){
my_mutex.lock();
cout << "线程执行,插入一个元素" << i << endl;
msgRecQue.push(i); //插入消息
my_mutex.unlock();
}
}
bool outMsgQue(int &num){
my_mutex.lock(); //给互斥量加锁
if(!msgRecQue.empty()){
num = msgRecQue.front();
msgRecQue.pop();
cout << "取出数据中" << num << endl;
my_mutex.unlock(); //解锁
return true;
}
my_mutex.unlock();
return false;
}
void outMsgRecQue(){
for(int i = 0;i < 1000;++i){
bool result = outMsgQue(i);
if(result){
cout << "outMsgRecQue被执行,取出一个元素" << i << endl;
}else{
cout << "消息队列为空" << endl;
}
}
cout << "end it!" << endl;
}
private:
//容器,用来收集代表玩家数据的队列
queue<int> MsgRecQue;
//定义一个互斥量
mutex my_mutex;
};
int main(){
//创建一个类对象
A myoj;
thread oj1(&A::inMsgRecQue,&myoj); //第一个参数为线程入口函数的地址,第二个参数是为了保证它们用的是同一个对象
thread oj2(&A::outMsgRecQue,&myoj);
oj1.join();
oj2.join();
return 0;
}
运行结果:
我们在一个类中定义了一个互斥量,并写了两个线程入口函数,一个负责往消息队列填入数据,一个负责取出数据,我们又在主线程中创建了两个线程,它们会不断去尝试抢占cpu,以得到运行的权力,如果没加锁,可能在in线程插入数据没执行完的时候,out线程就执行了,会造成数据不一致的现象。
我们分别给两个线程都上了锁,当in线程加锁后,out线程会检查互斥量的状态,因为它加了锁,所以out线程是无法获取锁的,它也就只能等待in线程执行完之前把锁解开,结束后再来执行out线程。这样就能保证数据的一致性。
函数简析
- mutex对象.lock(); 它的作用是给互斥量加锁,在加锁后,使用这个互斥量的其他线程将无法在它没运行完之前抢占cpu去运行。(用不好会产生死锁)
- mutex对象.unlock(); 它的作用是给互斥量解锁,锁上了就要打开嘛,在它解锁互斥量之后,其他线程就可以抢占cpu并尝试去加锁,尝试去运行。(有锁就有解锁,需成对使用)
线程死锁
…有俩线程A和B,它们各自可以对两把锁进行加锁,解锁,有两个互斥量,分别叫M1和M2。
(1)线程A执行的时候,这个线程先锁M1,把M1 lock成功了,然后它去lockM2。
此时出现了上下文切换(线程交替运行)
(2)线程B执行了,这个线程先锁M2,因为M2还没有被锁,所以M2会lock成功,线程B要去lockM1。
此时此刻,死锁就产生了;
(3)线程A因为锁不上M2,流程走不下去(所有后边代码有解锁M2的但是流程走不下去,所以M2解不开)
(4)线程B因为锁不上M1,流程走不下去(所有后边代码有解锁M1的但是流程走不下去,所以M1解不开)
//死锁演示
mutex M1;
mutex M2;
void funcA(){
M1.lock();
M2.lock();
M2.unlock();
M1.unlock();
}
void funB(){
M2.lock();
M1.lock();
M1.unlock();
M2.unlock();
}
拓展
lock
std:lock()函数模板:有用来处理多个互斥量的时候的能力:一次锁住两个或者两个以上的互斥量(至少两个,多了不限,1个不行);
lock(my_mutex1,my_mutex2); //一次给两个互斥量加锁
它不存在这种因为再多个线程中因为锁的顺序问题导致死锁的风险问题;
使用了std:lock() 后,如果互斥量中有一个没锁住,它就在那里等着,等所有互斥量都锁住,它才能往下走(返回);
要么两个互斥量都锁住,要么两个互斥量都没锁住。如果只锁了一个,另外一个没锁成功,则它立即把已经锁住的解锁。
因为它能一次锁多个互斥量,所以最好谨慎使用
lock_guard
C++引入了std::lock_gurad的类模板,用了lock_gurad就不能使用lock和unlock。lock_gurad在构造时会使用一次lock,在子进程结束,它析构的时候又会使用一次unlock。它的目的是为了防止我们使用lock后没使用unlock导致线程死锁。
void func(){
lock_guard<mutex> guard(my_mutex); //lock_gurad的使用
my_mutex.lock()
lock_guard<mutex> guard(my_mutex,std::adopt_lock); //有第二个参数的lock_gurad,需要预先加锁
//要执行的代码块
}
它可以接受两个参数,一个参数是互斥量对象,第二个是可选参数(std::adopt_lock),它表示lock_gurad在先前已经调用了lock给互斥量加锁,不需要再次加锁了。
unique_lock
unique_lock是一个类模板,功能类似,但它比lock_guard更加灵活,但占用内存大一点,效率低一点。它不能被复制给另一个同类型对象。(感觉就是拷贝构造函数被移除了一样)
void func(){
unique_lock<mutex> guard(my_mutex); //定义了一个unique_lock对象
}
unique_lock的第二个参数
- unique_lock<mutex(类型)> uguard(my_mutex,std::adopt_lock);
adopt_lock起标记作用,它表示参数里的互斥量已经被lock了,该函数将不会对其加锁,没加锁的话,则需要提前加锁 - unique_lock<mutex(类型)> uguard(my_mutex,std::try_to_lock);
使用了该参数后,它会尝试用mutex的lock去锁定这个mutex,若没有锁定成功,也会立即返回。不会阻塞, - unique_lock<mutex(类型)> uguard(my_mutex,std::defer_lock);
它会初始化一个没有锁的mutex,需要自己加锁,但不用担心解锁
unique_lock的成员函数
- lock(); 用于加锁
- unlock(); 用于解锁
- try_lock(); 尝试给互斥量加锁,拿不到则返回false,它不会像lock,拿不到就阻塞自己
- release(); 返回它所管理的mutex对象指针,并释放所有权,也就是说,该mutex对象将和lock不再有关系,我们有责任对该mutex对象解锁
unique_lock<mutex> rtn_unique_lock() {
unique_lock<mutex> tempguard(my_mutex);
return tempguard;
//从函数返回一个局部的unique_lock对象是可以的,
//它会使系统生成临时unique_lock对象,并调用unique_lock的移动构造函数,所以原来的互斥量也就不可用了,有点像迭代器的销毁。
}
总结
mutex可以帮我们去保证多个线程同时操纵共享数据的数据一致性,还介绍了一些能达成同样功能的其他类模板。我们在使用的时候也要注意,一定要理解自己写的代码究竟是什么意思,含义不明的代码最好不要去写,可以先去测试,弄明白作用,再酌情使用。