笔者近期在工作之中编程实现一个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. D - Milk Patterns (出现k次可重复的最长子串的长度)

    题目链接:https://cn.vjudge.net/contest/283743#problem/D 题目大意:给你n个数,然后问你出现m次的最长子串的长度. 具体思路:和上一篇博客的内容差不多,这 ...

  2. android 使用SQLite存储数据

    创建一个类继承SQLiteOpenHelper,重写他的构造方法.onCreate().onUpgrade() 构建出SQLiteOpenHelper实例后,再调用他的getReadableDatab ...

  3. Android BroadcastReceiver解析

    目录   示意图 1. 定义 即 广播,是一个全局的监听器,属于Android四大组件之一 Android 广播分为两个角色:广播发送者.广播接收者 2. 作用 监听 / 接收 应用 App 发出的广 ...

  4. IP分片丢失重传 - Sacrifice的日志 - 网易博客

        尽管IP分片看起来是是透明的,但有一点让人不想使用它:即使只丢失一片数据也要重传整个数据报.为什么会发生这种情况呢?     因为IP层本身没有超时重传的机制--由更高层来负责超时和重传(TC ...

  5. MySQL— 进阶

    目录 一.视图 二.触发器 三.函数 四.存储过程 五.事务 一.视图 视图是一个虚拟表(非真实存在),其本质是[根据SQL语句获取动态的数据集,并为其命名],用户使用时只需使用[名称]即可获取结果集 ...

  6. SharePoint 2010 使用Install-SPSolution部署wsp包状态一直是”正在部署”

    1.服务器场信息如下: 2.使用下面命令部署,状态一直是"正在部署" Install-SPSolution –Identity xxxx.wsp –WebApplication h ...

  7. mysql数据库查询库中所有表所占空间大小

    SELECT CONCAT(table_schema,'.',table_name) AS 'TABLE_NAME', CONCAT(, ),'M') AS 'ROW_SIZE', CONCAT( ) ...

  8. openwrt 添加 802.1x客户端njit

    1.修改feed的配置文件 feeds.conf.default 添加下面两句: src-svn njit https://github.com/liuqun/openwrt-clients/trun ...

  9. iframe传递参数问题

    在页面中嵌入了iframe,如果需要传递参数到iframe中 1.通过将参数嵌入到url中,在iframe中使用${param.xxx}可以获取 2.通过将参数存入到session中,在iframe中 ...

  10. totastmessage 触发事件后浮框消失的方法

    1. 前言 通过查了官放的文档,发现没有 totastmessage 触发事件后,浮框消失的方法,然后通过研究了下点击关闭时的源码,得到了一个的解决方案. 2. 样例代码如下 $("#dro ...