互斥量概念、用法、死锁演示及解决详解

一.互斥量的概念

保护共享数据,操作时,某个线程用代码把共享数据锁住、操作数据、解锁
其他线程想操作共享数据必须等待解锁。
互斥量
是个类对象,理解成一把锁,多个线程尝试用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()一次锁定多个互斥量;谨慎使用(建议一个一个锁),因为不同的互斥量控制不同的共享数据,两个互斥量在一起的情况不多见。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章