C++多线程:mutex互斥量与死锁(五)

1、互斥量的概念
  1. 互斥量是一个类对象,理解成一把锁,多个线程尝试用lock()成员函数来加锁这把锁头(成功的标志是lock()函数的返回,不成功将一直阻塞在这把锁上)。

    • 只有一个线程能锁定成功(成功的加锁),

    • 如果没有获取锁成功,那么该线程将会卡在lock()这个锁头上,不断的尝试去获取。

    • 互斥量需要小心使用,数据(临界区)不能过多或者过少的保护,过少有安全问题,过多有效率问题。

  2. 互斥量的用法

    • 先获取lock(),操作共享数据,unlock()释放
    • lock()和unlock()需要承兑使用,有lock()必然要有unlock
    • 每调用一次lock(),必须调用一次unlock(),否则会出现死锁
    • 不应该也不允许调用一次lock()却调用一次以上unlock(),即不允许非对称数量的操作
cpp 复制代码
#include <iostream>
#include <mutex>
#include <thread>
#include <queue>

class MessageQueue{
public:
    void inMsgRecvQueue(){
        for(int i = 0;i < n;i++){
            std::cout << "inMsgRecvQueue()执行, 插入一个元素i = " << i << std::endl;
            mutex_lock.lock();
            q.push(i);
            mutex_lock.unlock();
        }
    }

    void outMsgRecvQueue(){
        for(int i = 0;i < n;i++){
            int msg = poll_msg();
            if(msg > 0){
                std::cout << "outMsgRecvQueue()执行, msg = " << msg << std::endl;
            }
            else{
                std::cout << "outMsgRecvQueue()执行, 但此时消息队列中为空" << std::endl;
            }
        }
    }

    int poll_msg(){
        int msg = -1;
        mutex_lock.lock();
        if(q.size()){
            msg = q.front();
            q.pop();
            std::cout << "outMsgRecvQueue()执行, msg = " << msg << std::endl;
        }
        mutex_lock.unlock();
        return msg;
    }
private:
    std::queue<int> q;
    static const int n = 100000;
    std::mutex mutex_lock;
};

void test1()
{
    MessageQueue messageQueue;
    std::thread in_thread(&MessageQueue::inMsgRecvQueue, std::ref(messageQueue));
    std::thread out_thread(&MessageQueue::outMsgRecvQueue, std::ref(messageQueue));

    in_thread.join();
    out_thread.join();
}

针对所需要控制串行执行的临界区上锁,执行完毕之后再解锁即可保证。

2、std::lock_guard类模板
  • 为了防止使用lock()和unlock()进行非对称数量的操作,C++提供了mutex的类模板
  • std::lock_guard直接取代lock()和unlock(),直接使用lock_guard()代替二者
cpp 复制代码
template<typename _Mutex>
class lock_guard
{
public:
		typedef _Mutex mutex_type;

		explicit lock_guard(mutex_type& __m) : _M_device(__m){
            	_M_device.lock(); 
        }

		lock_guard(mutex_type& __m, adopt_lock_t) noexcept : _M_device(__m){ 
        
        } // calling thread owns mutex

		~lock_guard(){ 
            	_M_device.unlock(); 
        }
		lock_guard(const lock_guard&) = delete;
		lock_guard& operator=(const lock_guard&) = delete;

private:
		mutex_type&  _M_device;
};

这个是std::lock_guard的源码,可以看到lock_guard的源码很简单,在构造时加锁,析构时释放锁

  • 构造时加锁没有问题
  • 析构时候释放锁似乎不太合理
    • 一个类的析构是在其作用域失效时才会析构
    • 假设当前函数的代码非常长,那么意味着从构造到函数体全部执行完毕这一串都是临界区
    • 这样就会影响整个程序的运行效率
2.1、lock_guard灵活使用

由于防止lock_guard的生命周期过长,通常可以让他提前析构。提前析构的方式就是通过增加{}代码块,把需要临界区的代码放入{}中

cpp 复制代码
#include <iostream>
#include <mutex>
#include <thread>
#include <queue>

class Lock_Gurad{
public:
    void inMsgRecvQueue(){
        for(int i = 0;i < n;i++){
            std::cout << "inMsgRecvQueue()执行, 插入一个元素i = " << i << std::endl;
            {
                // 代码块,执行完lock_gurad析构
                std::lock_guard<std::mutex> mutex_lock_guard(mutex_lock);
                q.push(i);
            }
        }
    }

    void outMsgRecvQueue(){
        for(int i = 0;i < n;i++){
            int msg = poll_msg();
            if(msg > 0){
                std::cout << "outMsgRecvQueue()执行, msg = " << msg << std::endl;
            }
            else{
                std::cout << "outMsgRecvQueue()执行, 但此时消息队列中为空" << std::endl;
            }
        }
    }

    int poll_msg(){
        int msg = -1;
        // 这里需要等poll_msg()函数执行完才会释放lock_guard
        std::lock_guard<std::mutex> mutex_lock_guard(mutex_lock);
        if(q.size()){
            msg = q.front();
            q.pop();
            std::cout << "outMsgRecvQueue()执行, msg = " << msg << std::endl;
        }
        return msg;
    }
private:
    std::queue<int> q;
    static const int n = 100000;
    std::mutex mutex_lock;
};

void test_lock_guard()
{
    Lock_Gurad lockGurad;
    std::thread in_thread(&Lock_Gurad::inMsgRecvQueue, std::ref(lockGurad));
    std::thread out_thread(&Lock_Gurad::outMsgRecvQueue, std::ref(lockGurad));

    in_thread.join();
    out_thread.join();
}
3、死锁与预防
  • 产生死锁的必要条件:

    • 互斥条件:进程要求对所分配的资源进行排它性控制,即在一段时间内某资源仅为一进程所占用。

    • 请求和保持条件:当进程因请求资源而阻塞时,对已获得的资源保持不放。

    • 不可剥夺条件:进程已获得的资源在未使用完之前,不能剥夺,只能在使用完时由自己释放。

    • 循环等待条件:在发生死锁时,必然存在一个进程--资源的环形链。

  • 死锁的预防:

    • 资源一次性分配:一次性分配所有资源,这样就不会再有请求了:(破坏请求条件)
    • 只要有一个资源得不到分配,也不给这个进程分配其他的资源:(破坏请保持条件)
    • 可剥夺资源:即当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源(破坏不可剥夺条件)
    • 资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件)
3.1、死锁的演示
  • 线程1先拿锁1,再拿锁2
  • 线程2先拿锁2,再拿锁1
  • 相互持有一把锁,并且期望得到对方手里的锁,导致最后的死锁
cpp 复制代码
#include <iostream>
#include <mutex>
#include <thread>
#include <queue>

class DeadLock{
public:
    void inMsgRecvQueue(){
        for(int i = 0;i < n;i++){
            std::cout << "inMsgRecvQueue()执行, 插入一个元素i = " << i << std::endl;
            mutex_lock1.lock();
            mutex_lock2.lock();
            q.push(i);
            mutex_lock2.unlock();
            mutex_lock1.unlock();
        }
    }

    void outMsgRecvQueue(){
        for(int i = 0;i < n;i++){
            int msg = poll_msg();
            if(msg > 0){
                std::cout << "outMsgRecvQueue()执行, msg = " << msg << std::endl;
            }
            else{
                std::cout << "outMsgRecvQueue()执行, 但此时消息队列中为空" << std::endl;
            }
        }
    }

    int poll_msg(){
        int msg = -1;
        mutex_lock2.lock();
        mutex_lock1.lock();
        if(q.size()){
            msg = q.front();
            q.pop();
        }
        mutex_lock1.unlock();
        mutex_lock2.unlock();
        return msg;
    }
private:
    std::queue<int> q;
    static const int n = 100000;
    std::mutex mutex_lock1;
    std::mutex mutex_lock2;
};

void test_deadlock()
{
    DeadLock deadLock;
    std::thread in_thread(&DeadLock::inMsgRecvQueue, std::ref(deadLock));
    std::thread out_thread(&DeadLock::outMsgRecvQueue, std::ref(deadLock));

    in_thread.join();
    out_thread.join();
}
3.2、死锁的解决方法
  • 多个线程按照相同的顺序持有锁,例如都按照1,2或者2,1的顺序获取就没问题
  • std::lock模板函数,同时获取多把锁,下面是std::lock的源码
    • 其核心代码很简单也容易理解:当需要多把锁的时候,要么我同时拿到要么我一个也不要。

    • 即:如果能获取就获取,获取不到就释放全部的锁

    • 最后释放还是需要程序员手动释放两把锁

      template<typename _L1, typename _L2, typename... _L3>
      void lock(_L1& __l1, _L2& __l2, _L3&... __l3)
      {
      while (true){
      using __try_locker = __try_lock_impl<0, sizeof...(_L3) != 0>;
      unique_lock<_L1> __first(__l1);
      int __idx;
      auto __locks = std::tie(__l2, __l3...);
      __try_locker::__do_try_lock(__locks, __idx);
      if (__idx == -1){
      __first.release();
      return;
      }
      }
      }

因此可以将死锁代码改成下面这个样子

cpp 复制代码
void inMsgRecvQueue(){
    for(int i = 0;i < n;i++){
        std::cout << "inMsgRecvQueue()执行, 插入一个元素i = " << i << std::endl;
        std::lock(mutex_lock1, mutex_lock2);
        q.push(i);
        mutex_lock1.unlock();
        mutex_lock2.unlock();
    }
}

int poll_msg(){
    int msg = -1;
    std::lock(mutex_lock1, mutex_lock2);
    if(q.size()){
        msg = q.front();
        q.pop();
    }
    mutex_lock1.unlock();
    mutex_lock2.unlock();
    return msg;
}
3.3、std::adopt_lock类模板

而std::lock批量获取锁后为了忘记释放,可以在std::lock_guard中添加参数std::adopt_lock使得在在析构时自动释放锁

  • std::lock批量获取锁后, std::lock_guard()函数中加入std::adopt_lock将不会再获取锁,而是等待析构时释放
cpp 复制代码
void inMsgRecvQueue(){
    for(int i = 0;i < n;i++){
        std::cout << "inMsgRecvQueue()执行, 插入一个元素i = " << i << std::endl;
        std::lock(mutex_lock1, mutex_lock2);
        std::lock_guard<std::mutex>(mutex_lock1, std::adopt_lock);
        std::lock_guard<std::mutex>(mutex_lock2, std::adopt_lock);
        q.push(i);
    }
}

int poll_msg(){
    int msg = -1;
    std::lock(mutex_lock1, mutex_lock2);
    std::lock_guard<std::mutex>(mutex_lock1, std::adopt_lock);
    std::lock_guard<std::mutex>(mutex_lock2, std::adopt_lock);
    if(q.size()) {
        msg = q.front();
        q.pop();
    }
    return msg;
}
相关推荐
NuyoahC13 分钟前
算法笔记(十一)——优先级队列(堆)
c++·笔记·算法·优先级队列
jk_10115 分钟前
MATLAB中decomposition函数用法
开发语言·算法·matlab
weixin_4640780716 分钟前
C#串口温度读取
开发语言·c#
无敌の星仔18 分钟前
一个月学会Java 第2天 认识类与对象
java·开发语言
楚灵魈21 分钟前
[Linux]从零开始的网站搭建教程
linux·运维·服务器
小小不董23 分钟前
《Linux从小白到高手》理论篇:深入理解Linux的网络管理
linux·运维·服务器·数据库·php·dba
豆豆41 分钟前
为什么用PageAdmin CMS建设网站?
服务器·开发语言·前端·php·软件构建
FL16238631291 小时前
[C++]使用纯opencv部署yolov11-pose姿态估计onnx模型
c++·opencv·yolo
sukalot1 小时前
windows C++-使用任务和 XML HTTP 请求进行连接(一)
c++·windows
这可就有点麻烦了1 小时前
强化学习笔记之【TD3算法】
linux·笔记·算法·机器学习