c++11并发与多线程【第五节】互斥量概念,用法,死锁演示及解决

第五节 互斥量概念,用法,死锁演示及解决

关于进程和线程管理,这里讲的只限于这里的案例,更多的关于进程线程管理,死锁管理,同步互斥关系,强烈建议先去学习《操作系统》。

一、互斥量(mutex)的概念

互斥:当一个进程或线程使用共享数据时,另一个线程或进程必须等待,当占用共享数据的线程或进程退出后,另一个线程或进程才允许去访问该共享数据。实现方式:操作时,某个线程用代码把共享数据锁住,操作数据,解锁;其他想操作数据的线程必须等待解锁,然后锁住,操作,解锁。

互斥量是个类对象,理解成一把锁,多个线程会尝试使用这个对象的lock()成员函数来加锁这把锁头,但是只有一个线程能锁定成功,如果没锁成功,那么该线程便会阻塞在这个地方;

二、互斥量(mutex)的用法

互斥量包含在一个头文件中,使用是需要包含该头文件#include
新建一个互斥量:std::mutex my_mutex;

2.1 lock(),unlock()

步骤:先lock(),操作数据,然后unlock();

lock()与unlock()必须成对使用。

#include<iostream>
#include<vector>
#include<thread>
#include<string>
#include<list>
#include<mutex>
using namespace std;

class A{
public:
    //把收到的消息(玩家命令)放入到一个队列的线程入口函数
    void inMsgRecvQueue(){
        for(int i=0;i<10000;i++){//用数字模拟玩家发送来的命令
            cout<<"inMsgRecvQueue()执行,插入一个元素 "<<i<<endl;
            //
            my_mutex.lock();//加锁
            msgRecvQueue.push_back(i);//把命令放入队列当中
            my_mutex.unlock();//解锁
            //
        }
    }

    bool MsgProcess(int &command){

        my_mutex.lock();//加锁
        if(!msgRecvQueue.empty()){
            command=msgRecvQueue.front();//返回第一个元素
            msgRecvQueue.pop_front();//取出后移除该元素
            my_mutex.unlock();//解锁
            //然后处理数据
            return true;
        }
        my_mutex.unlock();//解锁

        return false;
    }
    //从消息队列list中读取玩家命令的线程入口函数
    void outMsgRecvQueue(){
        int command=0;
        for(int i=0;i<10000;i++){
            //
            bool result=MsgProcess(command);
            if(result){
                cout<<"outMsgRecvQueue执行,取出一个元素 "<<command<<endl;
                //然后对数据进行处理
            }
            else
                cout<<"outMsgRecvQueue执行,但是list已经空了 : "<<i<<endl;
        }
        cout<<"end "<<endl;
    }

private:
    std::list<int> msgRecvQueue;//在list中存放玩家发来的命令
    std::mutex my_mutex;
};

int main(){
    A myobj;
    std::thread myInMsgObj(&A::outMsgRecvQueue,&myobj);//第二个参数是引用,作用与std::ref相同,保证是子线程中使用的是主线程中的同一个对象,但是主线程后面必须等待子线程完成
    std::thread myOutMsgObj(&A::inMsgRecvQueue,&myobj);
    myInMsgObj.join();
    myOutMsgObj.join();

    cout<<"主线程结束"<<endl;
    return 0;
}

两个线程一次只能有一个能lock()成功,具体是那个lock()成功,由操作系统决定。

lock()与unlock()必须成对出现,为了防止lock后忘记unlock,C++引入了std::lock_guard的类模板,自动进行unlock;

2.2 std::lock_guard类模板:直接取代lock()和unlock()

一旦使用了lock_guard之后,便不能再使用lock和unlock;
使用方法:

std::lock_guard<std::mutex> lockguard(my_mutex);//lock_guard的类构造函数里执行了mutex::lock(),lock_guard的析构函数里执行了mutex::unlock()

lock_guard的类构造函数里执行了mutex::lock(),lock_guard的析构函数里执行了mutex::unlock();
例如:

bool MsgProcess(int &command){
    std::lock_guard<std::mutex> lockguard(my_mutex);//lock_guard的类构造函数里执行了mutex::lock(),lock_guard的析构函数里执行了mutex::unlock()
    
    //my_mutex.lock();//加锁
    if(!msgRecvQueue.empty()){
        command=msgRecvQueue.front();//返回第一个元素
        msgRecvQueue.pop_front();//取出后移除该元素
        //my_mutex.unlock();//解锁
        //然后处理数据
        return true;
    }
    //my_mutex.unlock();//解锁
    return false;
}

注意:lock_guard()的unlock发生在作用域结束之前,对与其作用域要特别注意。对共享数据的访问一定要限制在lock_guard的作用域内。

三、死锁

张三:站在北京 等李四
李四:站在深圳 等张三

3.1 死锁概念:两个或以上的进程或线程,在本身都占有资源的情况下,请求其他线程或进程占有的资源,形成了一个环,导致每个线程或进程都无法进行执行的情况。

死锁的详细概念与形成条件,可以看《操作系统》;
死锁的必要条件:

  1. 互斥,所分配的资源一次只能为一个进程或线程所占有;
  2. 非剥夺,进程或线程所获得的资源在为使用完毕之前,不能被其他进程或线程强行夺走;
  3. 请求并保持,进程或线程已经至少拥有了一个资源,同时又请求其他进程或线程所占有的资源;
  4. 循环等待,互相等待对方所占有资源形成了一个循环等待链,每个进程或线程已占有的资源被链中的下一个进程或线程所请求;

C++中,例子:
两个线程A,B:
线程A执行的时候,锁住了金锁,把金锁锁住成功了,然后去lock银锁;突然出现了上下文切换,线程B开始执行了,线程B先锁银锁,成功了,然后线程B去lock金锁;此时死锁就发生了。此时线程A因为锁不了银锁而阻塞,线程B因为锁不了金锁而阻塞。

死锁的情况:
线程1:先锁住了互斥量my_mutex1,再锁住my_mutex2

my_mutex1.lock();//加锁
//****如果中间有其他代码
my_mutex2.lock();
msgRecvQueue.push_back(i);//把命令放入队列当中
my_mutex1.unlock();//解锁
my_mutex2.unlock();

线程2:先锁住了互斥量my_mutex2,再锁住my_mutex1

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();

此时便会发生死锁。

3.2 死锁的解决办法

这里只从当前这个代码的角度来解决死锁,其他更详细的的死锁解决方法看《操作系统》

这里只需要保证线程一和线程二,加锁的顺序一致,就可以避免死锁。只要保证两个互斥量上锁的顺序一致就不会死锁。

3.3 std::lock()函数模板:处理多个互斥量时才出场

功能:一次锁住两个或两个以上的互斥量(至少两个,多了不限),但是一般使用情况比较少;
它不存在这种因为在多个线程中,因为锁的顺序导致死锁的风险问题。
std::lock(),如果要锁住的互斥量中有一个没锁住,就会阻塞,并且会把已经能锁住的互斥量解锁释放,等所有的互斥量都可以锁住之后,它才能往下走。
要么两个互斥量都能锁住,要么都没锁住

使用方法:

std::lock(my_mutex1,my_mutex2);//参数my_mutex1和my_mutex2顺序无所谓
//相关代码
my_mutex1.unlock();//解锁
my_mutex2.unlock();

3.4 std::lock_guard的std::adopt_lock参数

实际上类似于std::lock()与lock_guard()相结合的方法。

std::lock(my_mutex1,my_mutex2);
std::lock_guard<std::mutex> lockguard(my_mutex1,std::adopt_lock);//
std::lock_guard<std::mutex> lockguard(my_mutex2,std::adopt_lock);//
//相关代码
//my_mutex1.unlock();//解锁
//my_mutex2.unlock();
//

此时可以不用unlock();

std::adopt_lock参数是个结构体对象,起一个标记作用:表示这互斥量已经lock()过了,不需要在std::lock_guardstd::mutex的构造函数里对mutex对象进行lock了。

关于进程和线程管理,这里讲的只限于这里的案例,更多的关于进程线程管理,死锁管理,同步互斥关系,强烈建议先去学习《操作系统》。

文章内容来源《C++并发与多线程视频课程》

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2022-2024 lk
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信