笔者近期在工作之中编程实现一个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:

在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的封装:

这里有个事情挺奇怪的,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++多线程的相关内容,欢迎大家多多指教。

C++雾中风景12:聊聊C++中的Mutex,以及拯救生产力的Boost的更多相关文章

  1. 简单聊聊java中的final关键字

    简单聊聊java中的final关键字 日常代码中,final关键字也算常用的.其主要应用在三个方面: 1)修饰类(暂时见过,但是还没用过); 2)修饰方法(见过,没写过); 3)修饰数据. 那么,我们 ...

  2. 聊聊iOS中网络编程长连接的那些事

    1.长连接在iOS开发中的应用 常见的短连接应用场景:一般的App的网络请求都是基于Http1.0进行的,使用的是NSURLConnection.NSURLSession或者是AFNetworking ...

  3. go中sync.Mutex源码解读

    互斥锁 前言 什么是sync.Mutex 分析下源码 Lock 位运算 Unlock 总结 参考 互斥锁 前言 本次的代码是基于go version go1.13.15 darwin/amd64 什么 ...

  4. odi 12.2.1中访问excel文件

    由于在odi 12.2.1中,必须使用jdk1.8,而jdk1.8中jdbc-odbc bridge已经不再支持,因此,可以使用Progress DataDirect SequeLink来充当jdbc ...

  5. 在WINDOWS SERVER 上或远程桌面中使用 MUTEX

    引用: http://www.cnblogs.com/fg0711/archive/2012/05/03/2480502.html 使用Mutex需要注意的两个细节 可能你已经注意到了,例子中在给Mu ...

  6. 在Ubuntu 12.04系统中安装配置OpenCV 2.4.3的方法

    在Ubuntu 12.04系统中安装配置OpenCV 2.4.3的方法   对于,在Linux系统下做图像识别,不像在windows下面我们可以利用Matlab中的图像工具箱来实现,我们必须借助Ope ...

  7. 【转载】【时序约束学习笔记1】Vivado入门与提高--第12讲 时序分析中的基本概念和术语

    时序分析中的基本概念和术语 Basic concept and Terminology of Timing Analysis 原文标题及网址: [时序约束学习笔记1]Vivado入门与提高--第12讲 ...

  8. 12个 Linux 中 grep 命令的超级用法实例

    12个 Linux 中 grep 命令的超级用法实例 你是否遇到过需要在文件中查找一个特定的字符串或者样式,但是不知道从哪儿开始?那么,就请grep来帮你吧. grep是每个Linux发行版都预装的一 ...

  9. 12个Unity5中优化VR 应用的技巧

    本文章由cartzhang编写,转载请注明出处. 所有权利保留. 文章链接:http://blog.csdn.net/cartzhang/article/details/50176429 作者:car ...

随机推荐

  1. Django学习手册 - 自定义simple_tag / filter

    相关: Django的模板引擎提供一般性的功能函数,通过前端可以实现多数的代码逻辑功能,但它仅支持大多数常见情况下的函数功能,例如if判断,ifequal对比返回值等,复杂一些的函数功能并不支持,例如 ...

  2. D3开发中的资料整理

    D3开发台阶比较高,需要对html,css,js非常熟练,还要对SVG非常熟悉,SVG不会就不要开发D3了,下面给大家推荐一本资料,为大家未来的开发提供便利. 这个框架产品不支持ie8,是这个产品的特 ...

  3. css 背景图片自适应元素大小

    一.一种比较土的方法,<img>置于底层. 方法如下: CSS代码: HTML: <img src="背景图片路径" /> <span>字在背景 ...

  4. WPF版公司的自动签到程序

    1.外包公司要求早上签到,就写了一个自动打卡程序. 2.一直都想写这个程序,可是一直没有思路,知道有个软件公司找我做自动答题程序,于是自动打卡程序才应运而生.未来可以将自动答题程序开源工大家查看. 3 ...

  5. Linux mmc framework1:软件架构

    [部分内容来自] http://www.wowotech.net/comm/mmc_framework_arch.html 1. 前言 由eMMC基础技术1:MMC简介中MMC.SD.SDIO的介绍可 ...

  6. Caching漫谈--关于Cache的几个理论

    如今缓存是随处可见了,如果你的程序还没有使用到缓存,那可能是你的程序并发量很低,或对实时性要求很低.我们公司的ERP在显示某些报表时,每次打开都需要花上几分钟的时间,假如搜索引擎也是这么慢,我想这家搜 ...

  7. jmeter之ip欺骗

    说明:我看有的博客说官方文档是在jmeter2.5以上的版本有此功能的实现~ 我的是2.13版本,也可以实现 . 准备工作: 使用IP欺骗功能必须得本地有多个可用IP,通常普通的PC机只有一个物理网卡 ...

  8. LoadRunner性能测试入门教程

    javaweb性能测试那些事 一:什么是javaweb性能测试: 二:javaweb性能测试基本流程 三:javaweb性能测试常用指标: 1:响应时间:2-5-8 原则 2:吞吐量 3:资源使用率 ...

  9. tomcat 拒绝服务

    一 尝试重新下载二进制安装包安装包 wget http://mirror.bit.edu.cn/apache/tomcat/tomcat-9/v9.0.16/bin/apache-tomcat-9.0 ...

  10. bzoj营业额统计

    这个也是板子题吧,很水,求前驱后继即可 /* 插入,求前驱和后继 */ #include<iostream> #include<cstring> #include<cst ...