• 【学习笔记】C++并发与多线程笔记五:unique_lock详解


    一、前言

    本文接上文 【学习笔记】C++并发与多线程笔记四:互斥量(概念、用法、死锁) 的内容,主要纪录 unique_lock 的使用方法以及原理。

    二、uniqie_lock取代lock_quard

    uniqie_lock 是个类模板,它的功能跟 lock_quard 类似,但比 lock_quard 更灵活。在工作中,一般用 lock_quard (推荐使用)就足够了,但在一些特殊的场景下会用到 uniqie_lock。

    在上篇文章中讲到了 lock_quard 取代了 mutex 的 lock() 和 unlock(),在 lock_quard 的构造函数中上锁,在析构函数中解锁,这点其实在 uniqie_lock 中也是一样的。

    uniqie_lock 在使用上比 lock_quard 灵活,但代价就是效率会低一点,并且内存占用量也会相对高一些。

    uniqie_lock 的缺省用法实际上与 lock_quard 一样,可以直接替换,代码如下:

    #include <iostream>
    #include <thread>
    #include <mutex>
    #include <list>
    using namespace std;
    
    class A {
     public:
      /* 把收到的消息(玩家命令)存到队列中 */
      void inMsgRecvQueue() {
        for (int i = 0; i < 100000; ++i) {
          cout << "inMsgRecvQueue exec, push an elem " << i << endl;
          std::unique_lock<std::mutex> m_guard1(m_mutex1);
          msgRecvQueue.push_back(i); /* 假设数字 i 就是收到的玩家命令 */
        }
      }
      /* 消息队列不为空时,返回并弹出第一个元素 */
      bool outMsgLULProc(int& command) {
        std::unique_lock<std::mutex> m_guard1(m_mutex1);
        if (!msgRecvQueue.empty()) {
          command = msgRecvQueue.front(); /* 返回第一个元素 */
          msgRecvQueue.pop_front();       /* 移除第一个元素 */
          return true;
        }
        return false;
      }
      /* 把数据从消息队列中取出 */
      void outMsgRecvQueue() {
        int command = 0;
        for (int i = 0; i < 100000; ++i) {
          bool result = outMsgLULProc(command);
          if (result)
            cout << "outMsgLULProc exec, and pop_front: " << command << endl;
          else
            cout << "outMsgRecvQueue exec, but queue is empty!" << i << endl;
          cout << "outMsgRecvQueue exec end!" << i << endl;
        }
      }
    
     private:
      list<int> msgRecvQueue; /* 容器(实际上是双向链表):存放玩家发生命令的队列 */
      mutex m_mutex1;         /* 创建互斥量1 */
    };
    
    int main() {
      A obj;
      thread myInMsgObj(&A::inMsgRecvQueue, &obj);
      thread myOutMsgObj(&A::outMsgRecvQueue, &obj);
      myInMsgObj.join();
      myOutMsgObj.join();
    
      cout << "Hello World!" << endl;
      return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54

    三、uniqie_lock的第二个参数

    uniqie_lock 的第二个参数是一个标志位,其可取参数详见下文。

    3.1 std::adopt_lock

    该标记表示这个互斥锁已经被 lock() 了,uniqie_lock 不会再重复上锁。

    也就是说该标记的效果是:假设调用方线程已经拥有了互斥锁的所有权,通知 uniqie_lock 不需要再构造函数中 lock 这个互斥锁了。

      /* 把收到的消息(玩家命令)存到队列中 */
      void inMsgRecvQueue() {
        for (int i = 0; i < 100000; ++i) {
          cout << "inMsgRecvQueue exec, push an elem " << i << endl;
          m_mutex1.lock(); /* 先lock(),才能在 unique_lock 中用 adopt_lock 标准 */
          std::unique_lock<std::mutex> m_guard1(m_mutex1, std::adopt_lock);
          msgRecvQueue.push_back(i); /* 假设数字 i 就是收到的玩家命令 */
        }
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    备注:lock_quard 中该标记的含义相同。

    3.2 std::try_to_lock

    假设我们在 myOutMsgObj 线程的回调函数中拿到互斥锁后,sleep 20 秒,就存在一个问题,myOutMsgObj 线程占用了互斥锁资源,却不向下执行,导致另一条线程 myInMsgObj 一直都没办法拿到互斥锁,也要等 20 秒,造成计算资源浪费。

      /* 消息队列不为空时,返回并弹出第一个元素 */
      bool outMsgLULProc(int& command) {
        std::unique_lock<std::mutex> m_guard1(m_mutex1);
    
        std::chrono::milliseconds dura(20000); /* 20秒 */
        std::this_thread::sleep_for(dura);     /* sleep 20秒 */
    
        if (!msgRecvQueue.empty()) {
          command = msgRecvQueue.front(); /* 返回第一个元素 */
          msgRecvQueue.pop_front();       /* 移除第一个元素 */
          return true;
        }
        return false;
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    为解决这个问题,uniqie_lock 引入了 try_to_lock 参数,它表示代码会尝试上锁,即使没有成功,也会立即返回,不会阻塞的等待。

      /* 把收到的消息(玩家命令)存到队列中 */
      void inMsgRecvQueue() {
        for (int i = 0; i < 100000; ++i) {
          cout << "inMsgRecvQueue exec, push an elem " << i << endl;
          /* 尝试上锁 */
          std::unique_lock<std::mutex> m_guard1(m_mutex1, std::try_to_lock);
          if (m_guard1.owns_lock()) {  /* 如果拿到了锁 */
            msgRecvQueue.push_back(i); /* 假设数字 i 就是收到的玩家命令 */
          } else {
            cout << "try_to_lock fail, do something else!!!" << endl;
          }
        }
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    备注:使用 try_to_lock 参数前,线程中不能调用 lock(),否则会造成死锁。

    3.3 std::defer_lock

    该标记表示并没有给 mutex 加锁,即初始化了一个没有加锁的 mutex。使用该标记初始化的 uniqie_lock 对象可以灵活的调用 uniqie_lock 的成员函数,这点将在下文中一起演示。

    四、uniqie_lock的成员函数

    4.1 lock()

    使用 std::defer_lock 参数初始化 uniqie_lock 对象可以调用unique_lock的成员函数上锁,并且无需在代码中解锁,它会自动解锁,有点类似智能指针。

      /* 把收到的消息(玩家命令)存到队列中 */
      void inMsgRecvQueue() {
        for (int i = 0; i < 100000; ++i) {
          cout << "inMsgRecvQueue exec, push an elem " << i << endl;
          /* 初始化了没有加锁的m_mutex1 */
          std::unique_lock<std::mutex> m_guard1(m_mutex1, std::defer_lock);
          m_guard1.lock(); /* 调用unique_lock的成员函数上锁,并且无需在代码中解锁,它会自动解锁 */
          msgRecvQueue.push_back(i); /* 假设数字 i 就是收到的玩家命令 */
        }
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    4.2 unlock()

    根据 4.1 的代码,我们可以知道 unique_lock 的成员函数 lock() 上锁后,在对象析构的时候会自动解锁,那为什么 unique_lock 还需要提供 unlock() 函数呢?

    这里就体现了 unique_lock 的灵活性,它既具备智能指针自动销毁的特性,又可以在代码中手动解锁,方便程序在一段代码中对其中某几个部分上锁的需求,将一些非共享代码从锁中提取出来,从而提高效率。

    备注:通常,在代码中,上锁的代码越少(粒度越小),效率越高。

      /* 把收到的消息(玩家命令)存到队列中 */
      void inMsgRecvQueue() {
        for (int i = 0; i < 100000; ++i) {
          cout << "inMsgRecvQueue exec, push an elem " << i << endl;
          /* 初始化了没有加锁的m_mutex1 */
          std::unique_lock<std::mutex> m_guard1(m_mutex1, std::defer_lock);
          m_guard1.lock(); /* 调用unique_lock的成员函数上锁,并且无需在代码中解锁,它会自动解锁 */
          msgRecvQueue.push_back(i); /* 假设数字 i 就是收到的玩家命令 */
          m_guard1.unlock();
          /* 非共享代码...... */
          m_guard1.lock();
          /* 共享代码...... */
          m_guard1.unlock();  /* 这个unlock() 可调可不调,m_guard1 对象析构时会自动解锁 */
        }
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    4.3 try_lock()

    这个成员函数与 std::try_to_lock 参数类似,尝试上锁,如果拿到锁了,则返回 true,否则返回 false。

      /* 把收到的消息(玩家命令)存到队列中 */
      void inMsgRecvQueue() {
        for (int i = 0; i < 100000; ++i) {
          cout << "inMsgRecvQueue exec, push an elem " << i << endl;
          /* 初始化了没有加锁的m_mutex1 */
          std::unique_lock<std::mutex> m_guard1(m_mutex1, std::defer_lock);
          if (m_guard1.try_lock()) {   /* 如果拿到锁了 */
            msgRecvQueue.push_back(i); /* 假设数字 i 就是收到的玩家命令 */
          } else {
            cout << "try_to_lock fail, do something else!!!" << endl;
          }
        }
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    4.4 release()

    该成员函数的作用是返回它所管理的 mutex 对象指针,并释放所有权;也就是说,这个 unique_lock 和 mutex 不再有关系。

    调用以下代码创建 unique_lock 类型的对象 m_guard1 时,实际上是把 m_guard1 与 m_mutex1 对象绑定在一起了,release() 函数可以将这两个对象解绑,并返回绑定的 mutex 指针,即此处的 m_mutex1 。注意这里是解绑,并不是销毁。

    std::unique_lock<std::mutex> m_guard1(m_mutex1);
    
    • 1

    调用 release() 函数解绑后,我们必须保存返回的 mutex 指针,并在接下来的代码中自行管理。

      /* 把收到的消息(玩家命令)存到队列中 */
      void inMsgRecvQueue() {
        for (int i = 0; i < 100000; ++i) {
          cout << "inMsgRecvQueue exec, push an elem " << i << endl;
          /* 初始化了没有加锁的m_mutex1 */
          std::unique_lock<std::mutex> m_guard1(m_mutex1);
          std::mutex* pmutex = m_guard1.release(); /* 解绑后返回之前绑定的m_mutex1,接下来我们要自行管理m_mutex1 */
          msgRecvQueue.push_back(i); /* 假设数字 i 就是收到的玩家命令 */
          pmutex->unlock();          /* 解绑后需要自己解锁 */
        }
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    五、uniqie_lock所有权的传递

    当 uniqie_lock 与 mutex 对象绑定在一起才是一个完整的、能发挥作用的 uniqie_lock 实例,也就是说 uniqie_lock 需要和 mutex 配合使用,可以理解为 uniqie_lock 需要管理一个 mutex。

    一个 mutex 对象只能被一个 uniqie_lock 对象所有(拥有),即同一个 mutex 无法被两个 uniqie_lock 对象同时使用,这就是 uniqie_lock 的所有权概念。

    /* 上锁两次,造成死锁 */
    std::unique_lock<std::mutex> m_guard1(m_mutex1);
    std::unique_lock<std::mutex> m_guard2(m_mutex1); /* 复制所有权(程序崩溃) */
    
    • 1
    • 2
    • 3

    m_guard1 拥有 m_mutex1 的所有权,并且 m_guard1 可以把自己 mutex(m_mutex1) 的所有权转移给其他的 uniqie_lock 对象。

    备注:所有权可以转移,但不能复制,这与智能指针 unique_ptr 类似,智能指针指向的对象同样也是可以移动,但不能复制。

    将 m_guard1 的所有权转移给 m_guard2,代码如下:

    std::unique_lock<std::mutex> m_guard1(m_mutex1);
    /* 移动语义,相当于m_guard1与m_mutex1解绑;m_guard2与m_mutex1绑定 */
    std::unique_lock<std::mutex> m_guard2(std::move(m_guard1)); 
    
    • 1
    • 2
    • 3

    转移unique_lock所有权的扩展方法:

      std::unique_lock<std::mutex> rtn_unique_lock() {
        std::unique_lock<std::mutex> temp_guard(m_mutex1);
        /* 从函数返回一个局部的temp_guard(移动构造函数)*/
        /* 返回局部对象temp_guard会让系统创建临时的unique_lock对象,并调用unique_lock的移动构造函数 */
        return temp_guard;
      }
      /* 把收到的消息(玩家命令)存到队列中 */
      void inMsgRecvQueue() {
        for (int i = 0; i < 100000; ++i) {
          cout << "inMsgRecvQueue exec, push an elem " << i << endl;
          /* rtn_unique_lock 函数中 temp_guard 对象的所有权转移到 m_guard1 中了 */
          std::unique_lock<std::mutex> m_guard1 = rtn_unique_lock();
          msgRecvQueue.push_back(i); /* 假设数字 i 就是收到的玩家命令 */
        }
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
  • 相关阅读:
    Prometheus抓取springBoot指标并grafana可视化
    HTTP协议笔记
    树的应用 —— 树的简介
    KRONES克朗斯电源维修0-901-17-350-8技术概论
    【docker】基于dockerfile编写LNMP
    一款纯前端类似excel的在线表格
    SwiftUI 4 新功能 之 网格视图Gridview组件 (教程含源码)
    一面锦旗一个故事——百华真情暖人心,职工感恩送锦旗
    Kali安装pip以及pip换源
    【代码随想录】栈与队列专栏(java版本)
  • 原文地址:https://blog.csdn.net/weixin_40026797/article/details/125380206