C++雾中风景12:聊聊C++中的Mutex,以及拯救生产力的Boost

笔者近期在工作之中编程实现一个Cache结构的封装,需要使用到C++之中的互斥量Mutex,于是花了一些时间进行了调研。(结果对C++标准库很是绝望....)最终还是通过利用了Boost库的shared_mutex解决了问题。借这个机会来聊聊在C++之中的多线程编程的一些“坑”

1.C++多线程编程的困扰

C++从11开始在标准库之中引入了线程库来进行多线程编程,在之前的版本需要依托操作系统本身提供的线程库来进行多线程的编程。(其实本身就是在标准库之上对底层的操作系统多线程API统一进行了封装,笔者本科时进行操作系统实验是就是使用的pthread或<windows.h>来进行多线程编程的
提供了统一的多线程固然是好事,但是标准库给的支持实在是有限,具体实践起来还是让人挺困扰的:

  • C++本身的STL并不是线程安全的。所以缺少了类似与Java并发库所提供的一些高性能的线程安全的数据结构。(Doug Lea大神亲自操刀完成的并发编程库,让JDK5成为Java之中里程碑式的版本)
  • 如果没有线程安全的数据结构,退而求其次,可以自己利用互斥量Mutex来实现。C++的标准库支持如下的互斥量的实现:
互斥量 版本 作用
mutex C++11 最基本的互斥量
timed_mutex C++11 有超时机制的互斥量
recursive_mutex C++11 可重入的互斥量
recursive_timed_mutex C++11 结合 2,3 特点的互斥量
shared_timed_mutex C++14 具有超时机制的可共享互斥量
shared_mutex C++17 共享的互斥量

由上述表格可见,C++是从14之后的版本才正式支持共享互斥量,也就是实现读写锁的结构。由于笔者的公司仅支持C++11的版本,所以就没有办法使用共享互斥量来实现读写锁了。所以最终笔者只好求助与boost的库,利用boost提供的读写锁来完成了所需完成的工作。(所以对工具不足时可以考虑求助于boost库,确实是解放生产力的大杀器,C++的标准库实在太简陋了~~)

2.标准库互斥量的剖析

虽然吐槽了一小节,但并不影响继续去学习C++标准库给我们提供的工具.........(但愿公司能再推动升级一波C++的版本~~不过看起来是遥遥无期了)接下来笔者就要来带领大家简单剖析一些C++标准库之中互斥量。

mutex

mutex的中文翻译就是互斥量,很多人喜欢称之其为锁。其实不是太准确,因为多线程编程本质上应该通过互斥量之上加锁,解锁的操作,来实现多线程并发执行时对互斥资源线程安全的访问。 我们来看看mutex类的使用方法:

long num = 0;
std::mutex num_mutex;

void numplus() {
    num_mutex.lock();
    for (long i = 0; i < 1000000; ++i) {
        num++;
    }
    num_mutex.unlock();
};

void numsub() {
    num_mutex.lock();
    for (long i = 0; i < 1000000; ++i) {
        num--;
    }
    num_mutex.unlock();
}

int main() {
    std::thread t1(numplus);
    std::thread t2(numsub);
    t1.join();
    t2.join();
    std::cout << num << std::endl;
}

调用线程从成功调用lock()或try_lock()开始,到unlock()为止占有mutex对象。当存在某线程占有mutex时,所有其他线程若调用lock则会阻塞,而调用try_lockh会得到false返回值。由上述代码可以看到,通过mutex加锁的方式,来确保只有单一线程对临界区的资源进行操作。
time_mutex与recursive_mutex的使用也是大同小异,两者都是基于mutex来实现的。( 本质上是基于recursive_mutex实现的,mutex为recursive_mutex的特例)
time_mutex则是进行加锁时可以设置阻塞的时间,若超过对应时长,则返回false。
recursive_mutex则让单一线程可以多次对同一互斥量加锁,同样,解锁时也需要释放相同多次的锁。
以上三种类型的互斥量都是包装了操作系统底层的pthread_mutex_t:

pthread_mutex_t结构

在C++之中并不提倡我们直接对锁进行操作,因为在lock之后忘记调用unlock很容易造成死锁。而对临界资源进行操作时,可能会抛出异常,程序也有可能break,return 甚至 goto,这些情况都极容易导致unlock没有被调用。所以C++之中通过RAII来解决这个问题,它提供了一系列的通用管理互斥量的类:

互斥量管理 版本 作用
lock_graud C++11 基于作用域的互斥量管理
unique_lock C++11 更加灵活的互斥量管理
shared_lock C++14 共享互斥量的管理
scope_lock C++17 多互斥量避免死锁的管理

创建互斥量管理对象时,它试图给给定mutex加锁。当程序离开互斥量管理对象的作用域时,互斥量管理对象会析构并且并释放mutex。所以我们则不需要担心程序跳出或产生异常引发的死锁了。
对于需要加锁的代码段,可以通过{}括起来形成一个作用域。比如上述代码的栗子,可以进行如下改写(推荐):

long num = 0;
std::mutex num_mutex;

void numplus() {
    std::lock_guard<std::mutex> lock_guard(num_mutex);
    for (long i = 0; i < 1000000; ++i) {
        num++;
    }
};
void numsub() {
    std::lock_guard<std::mutex> lock_guard(num_mutex);
    for (long i = 0; i < 1000000; ++i) {
        num--;
    }
}

int main() {
    std::thread t1(numplus);
    std::thread t2(numsub);
    t1.join();
    t2.join();
    std::cout << num << std::endl;
}

由上述代码可以看到,代码结构变得更加明晰了,对于锁的管理也交给了程序本身来进行处理,减少了出错的可能。

shared_mutex

C++14的版本之后提供了共享互斥量,它的区别就在于提供更加细粒度的加锁操作:lock_sharedlock_shared是一个获取共享锁的操作,而lock是一个获取排他锁的操作,通过这种方式更加细粒度化锁的操作。shared_mutex也是基于操作系统底层的读写锁pthread_rwlock_t的封装:

pthread_rwlock_t的结构

这里有个事情挺奇怪的,C++14提供了shared_timed_mutex 而在C++17提供了shared_mutex。其实shared_timed_mutex涵盖了shard_mutex的功能。(不知道是不是因为名字被diss了,所以后续在C++17里将shared_mutex**加了回来)。共享互斥量适用与读多写少的场景,举个栗子:

long num = 0;
std::shared_mutex num_mutex;

// 仅有单个线程可以写num的值。
void numplus() {
    std::unique_lock<std::shared_mutex> lock_guard(num_mutex);
    for (long i = 0; i < 1000000; ++i) {
        num++;
    }
};

// 多个线程同时读num的值。
long numprint() {
    std::shared_lock<std::shared_mutex> lock_guard(num_mutex);
    return num;
}

简单来说:

  • shared_lock是读锁。被锁后仍允许其他线程执行同样被shared_lock的代码
  • unique_lock是写锁。被锁后不允许其他线程执行被shared_lock或unique_lock的代码。它可以同时限制unique_lock与share_lock

不得不说,C++11没有将共享互斥量集成进来,在很多读多写少的应用场合之中,标准库本身提供的锁机制显得很鸡肋,也从而导致了笔者最终只能求助与boost的解决方案。(其实也可以通过标准库的mutex来实现一个读写锁,这也是面试笔试之中常常问到的问题。不过太麻烦了,还得考虑和互斥量管理类兼容什么的,果断放弃啊)

多锁竞争

还剩下最后一个要写的内容:scope_lock ,当我们要进行多个锁管理时,很容易出现问题,由于加锁的先后顺序不同导致死锁。(其实本来不想写了,好累。这里就简单用例子做解释吧,偷个懒~~)
如下栗子,加锁顺序不当导致死锁:

std::mutex m1, m2;
// thread 1
{
  std::lock_guard<std::mutex> lock1(m1);
  std::lock_guard<std::mutex> lock2(m2);
}
// thread 2
{
  std::lock_guard<std::mutex> lock2(m2);
  std::lock_guard<std::mutex> lock1(m1);
}

而通过C++17提供的scope_lock就可以很简单解决这个问题了:

std::mutex m1, m2;
// thread 1
{
  std::scope_lock lock(m1, m2);
}
// thread 2
{
  std::scope_lock lock(m1, m2);
}

好吧,妈妈再也不用担心我会死锁了~~

3.小结

算是简单的梳理完C++标准库之中的mutex了,也通过一些栗子比较完整的展现了使用方式。笔者上述关于标准库的内容,在boost库之中都能找到对应的实现,不过如果能够使用标准库,尽量还是不要引用boost了。(走投无路的时候记得求助boost,真香~~)希望大家在实践之中可以很好的运用好这些C++互斥量来更好的确保线程安全了。后续笔者还会继续深入的探讨有关C++多线程的相关内容,欢迎大家多多指教。

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

推荐阅读更多精彩内容