【学习笔记】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;
}

三、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 就是收到的玩家命令 */
    }
  }

备注: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;
  }

为解决这个问题,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;
      }
    }
  }

备注:使用 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 就是收到的玩家命令 */
    }
  }

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 对象析构时会自动解锁 */
    }
  }

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;
      }
    }
  }

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

调用 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();          /* 解绑后需要自己解锁 */
    }
  }

五、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); /* 复制所有权(程序崩溃) */

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

转移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 就是收到的玩家命令 */
    }
  }
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,137评论 6 511
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,824评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,465评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,131评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,140评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,895评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,535评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,435评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,952评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,081评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,210评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,896评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,552评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,089评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,198评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,531评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,209评论 2 357

推荐阅读更多精彩内容