C++多线程中互斥量的使用
多线程中互斥信号量(Mutex)的使用
1.0 互斥量的基本概念
1.1 Example
\(\quad\)首先我们要明白,为什么会有互斥信号量的出现,在多线程编程中,不同的线程之间往往要对同一个数据进行操作,如果该数据是只读的,当然不会出现什么问题,但是如果两个线程同时对某个数据进行写操作,则可能出现难以预料的事情。
- 我们来看一个简单的操作
#include <atomic>
#include <iostream>
#include <thread>
#include <chrono>
#include <pthread.h>
using namespace std;
int i = 0;
const int maxCnt = 1000000;
void mythread()
{
for (int j = 0; j < maxCnt; j++)
{
i++; // 线程同时操作变量
}
}
int main()
{
auto begin = chrono::high_resolution_clock::now();
thread t1(mythread);
thread t2(mythread);
t1.join();
t2.join();
auto end = chrono::high_resolution_clock::now();
cout << "i=" << i << endl;
cout << "time: "
<< chrono::duration_cast<chrono::microseconds>(end - begin).count() *
1e-6
<< "s" << endl; // 秒计时
}
可以看到在我的电脑上程序的输出为
i=1022418
time: 0.010445s
很明显和我们预想的结果是不一致的,我们使用两个线程同时对该变量进行加法操作,根据运行此书来计算,结果因该为 2000000,但事实上却不是这样的,这就是因为有多个线程在对同一个变量进行写操作的时候会出现难以排查的问题,意想不到的结果。此时mutex就派上用场了,我们对程序进行稍微的改动。
std::mutex var_mutex;
int i = 0;
const int maxCnt = 1000000;
void mythread()
{
for (int j = 0; j < maxCnt; j++)
{
var_mutex.lock();
i++; // 线程同时操作变量
var_mutex.unlock();
}
}
此时再运行程序可以发现结果如下,这是符合我们的预期的。
i=2000000
time: 0.09337s
1.2 互斥量用法解释
\(\quad\)互斥量就是个类对象,可以理解成一把锁,多个线程尝试用lock()成员函数来加锁,从而获得对数据的访问权限,或者说读写权限,其实就是继续执行代码的权限。最终只有一个线程能锁定成功,如果没有锁成功,那么流程将卡在lock()这里不断尝试去锁定。所以我们在使用的时候要注意尽量在lock()和unlock()中间插入较小的代码片段,这样才能提高多线程时程序的执行效率,比如前面的加加操作,只有一行,如果你在锁住某个线程之后后面在unlock之前还让线程睡眠了一会,那你可真是个大聪明。当然也有其他的方法可以跳过等待,后面我们也会说到。
\(\quad\)在使用互斥量的时候要包含头文件 #include<mutex> 然后使用mutex 类即可创建对象。更重要的一点是,在代码中 lock() (上锁)和 unlock() (解锁)必须成对使用,代码中使用互斥量的时绝不允许非对称调用,即 lock() 和 unlock() 一定是成对出现的。步骤如下:
- 先
lock()上锁; - 然后操作共享数据;
- 再
unlock()解锁
2.0其他C++11新特性
2.1 std::lock_guard类模板
\(\quad\)我们在代码中上锁后,一定要记得解锁,如果忘记解锁会导致程序运行异常,而且通常很难排查。为了防止开发者忘记解锁,C++11引入了一个叫做 std::lock_guard 的类模板,它在开发者忘记解锁的时候,会替开发者自动解锁。std::lock_guard 可以直接取代 lock() 和 unlock(),也就说使用 std::lock_guard 后,就不能再使用 lock() 和 unlock() 了。
如下所示
std::mutex var_mutex; int i = 0;
const int maxCnt = 1000000;
void mythread()
{
for (int j = 0; j < maxCnt; j++)
{
lock_guard<mutex> guard(var_mutex);
i++; // 线程同时操作变量
}
}
输出结果为
i=2000000
time: 0.102605s
\(\quad\)std::lock_guard 虽然用起来方便,但是不够灵活,它只能在析构函数中 unlock(),也就是对象被释放的时候,这通常是在函数返回的时候,或者通过添加代码块 { /* 代码块 */ } 限定作用域来指定释放时机。其还有一个特性是在构造的时候可以传入第二个参数为std::adopt_lock,此时在析构的时候就不会unlock了,但是此时就必须我们手动unlock了,这种使用场景也不多。
2.2 死锁
\(\quad\)谈到互斥量,就不得不来说一下死锁。一个简单的例子:
- 张三在北京说:等李四来了之后,我就去广东。
- 李四在广东说:等张三来了之后,我就去北京。
\(\quad\)张三李四互相扯皮,两人一直互相等待,就死等()。同理,假设代码中有两把锁,至少有两个互斥量存在才会产生死锁,分别称为锁1、锁2,并且有两个线程分别称为线程A和线程B。只有在某个线程同时获得锁1和锁2时,才能完成某项工作:
- 线程A执行时,先上锁1------------再上锁2。
- 线程B执行时,先上锁2------------再上锁1。
\(\quad\)如果在执行线程A的时候,先对1上了锁,这时候出现了上下文切换(并不是说上锁之后该线程不会被其他线程占用,而是说其他线程执行到需要锁1的时候如果发现被锁了,会给出执行权),现在来到了线程B,线程需要对锁2上锁,进行数据操作,此时发现锁2没有被锁,则上锁,继续执行发现,需要上锁1,但是此时的锁1被线程A锁上了,于是只能给出执行权限,此时又回到了线程1,线程1发现我如果想继续执行,那么就又要给2上锁,但是发现锁2又被线程B给上锁了,于是也只好给出执行权限。就这样,两个线程来回扯皮,就形成了死锁。
\(\quad\)用一句话概括以下呢就是:在程序执行线程A的过程中,上好了锁1后,出现了上下文切换,系统调度转去执行线程B,把锁2给上了,那么后续线程A拿不到锁2,线程B拿不到锁1,两条线程都没法往下执行,即出现了死锁。
- 例如下面的程序
#include <pthread.h>
#include <atomic>
#include <chrono>
#include <iostream>
#include <mutex>
#include <thread>
#include <list>
using namespace std;
list<int> msgRecvQueue; // 容器(实际上是双向链表):存放玩家发生命令的队列
mutex m_mutex1; // 创建互斥量1
mutex m_mutex2; // 创建互斥量2
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i)
{
cout << "inMsgRecvQueue exec, push an elem " << i << endl;
m_mutex1.lock(); // 实际代码中,两把锁不一定同时上,它们可能保护不同的数据
m_mutex2.lock();
msgRecvQueue.push_back(i); // 假设数字 i 就是收到的玩家命令
m_mutex2.unlock();
m_mutex1.unlock();
}
}
bool outMsgLULProc(int &command)
{
m_mutex2.lock();
m_mutex1.lock();
if (!msgRecvQueue.empty())
{
command = msgRecvQueue.front(); // 返回第一个元素
msgRecvQueue.pop_front(); // 移除第一个元素
m_mutex1.unlock();
m_mutex2.unlock();
return true;
}
m_mutex1.unlock();
m_mutex2.unlock();
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;
}
}
int main()
{
thread myInMsgObj(inMsgRecvQueue);
thread myOutMsgObj(outMsgRecvQueue);
myInMsgObj.join();
myOutMsgObj.join();
cout << "Hello World!" << endl;
return 0;
}
笔者运行的时候发现程序会卡死,无法输出最后的一句话
outMsgLULProc exec, and pop_front: 271
outMsgRecvQueue exec end!289
outMsgLULProc exec, and pop_front: 272
outMsgRecvQueue exec end!290
inMsgRecvQueue exec, push an elem 491
通常来讲,死锁的一般解决方案,只要保证多个互斥量上锁的顺序一致,就不会出现死锁,比如把上面示例代码的两个线程回调函数中的上锁顺序改一下,保持一致就好了(都改为先上锁1,再上锁2)。读者可以自己试一下改动下代码。
- 线程A执行时,先上锁1------------再上锁2。
- 线程B执行时,先上锁1------------再上锁2。
这样的顺序之下就形不成死锁了。因为当切换到B的时候B由于没有锁1所以值接让出执行权限。
2.3 死锁的另一种解决方案
std::lock() 函数模板是C++11引入的,它能一次锁住两个或两个以上的互斥量,并且它不存在上述的在多线程中由于上锁顺序问题造成的死锁现象,原因如下:std::lock() 函数模板在锁定两个互斥量时,只有两种情况:
- 两个互斥量都没有锁住;
- 两个互斥量都被锁住。
如果只锁了一个,另一个没锁成功,则它会立即把已经锁住的互斥量解锁。将上面的接收函数改为如下就可以避免死锁的出现。
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i)
{
cout << "inMsgRecvQueue exec, push an elem " << i << endl;
// m_mutex1.lock(); // 实际代码中,两把锁不一定同时上,它们可能保护不同的数据
// m_mutex2.lock();
std::lock(m_mutex1,m_mutex2);
msgRecvQueue.push_back(i); // 假设数字 i 就是收到的玩家命令
m_mutex2.unlock();
m_mutex1.unlock();
}
}
在使用 std::lock() 函数模板锁上多个互斥量时,也必须得记得把每个互斥量解锁,此时借助 std::lock_guard 的 std::adopt_lock 参数可以省略解锁的代码。我们再稍微更改一下代码,让他看上去更modern一些。
void inMsgRecvQueue()
{
for (int i = 0; i < 100000; ++i)
{
cout << "inMsgRecvQueue exec, push an elem " << i << endl;
// m_mutex1.lock(); // 实际代码中,两把锁不一定同时上,它们可能保护不同的数据
// m_mutex2.lock();
std::lock(m_mutex1, m_mutex2); // 锁上两个互斥量
std::lock_guard<std::mutex> m_guard1(m_mutex1, std::adopt_lock); // 构造时不上锁,但析构时解锁
std::lock_guard<std::mutex> m_guard2(m_mutex2, std::adopt_lock); // 构造时不上锁,但析构时解锁
msgRecvQueue.push_back(i); // 假设数字 i 就是收到的玩家命令
}
}
Reference
- https://blog.csdn.net/weixin_40026797/article/details/123974378
- https://blog.csdn.net/qq_24447809/article/details/118179908?spm=1001.2014.3001.5506
C++多线程中互斥量的使用的更多相关文章
- Linux多线程——使用互斥量同步线程
前文再续,书接上一回,在上一篇文章: Linux多线程——使用信号量同步线程中,我们留下了一个如何使用互斥量来进行线程同步的问题,本文将会给出互斥量的详细解说,并用一个互斥量解决上一篇文章中,要使用两 ...
- windows多线程同步--互斥量
关于互斥量的基本概念:百度百科互斥量 推荐参考博客:秒杀多线程第七篇 经典线程同步 互斥量Mutex 注意:互斥量也是一个内核对象,它用来确保一个线程独占一个资源的访问.互斥量与关键段的行为非常相似, ...
- Linux多线程--使用互斥量同步线程【转】
本文转载自:http://blog.csdn.net/ljianhui/article/details/10875883 前文再续,书接上一回,在上一篇文章:Linux多线程——使用信号量同步线程中, ...
- pthread中互斥量,锁和条件变量
互斥量 #include <pthread.h> pthread_mutex_t mutex=PTHREAD_MUTEX_INTIIALIZER; int pthread_mutex_in ...
- 多线程相关------互斥量Mutex
互斥量(Mutex) 互斥量是一个可以处于两态之一的变量:解锁和加锁.只有拥有互斥对象的线程才具有访问资源的权限.并且互斥量可以用于不同进程中的线程的互斥访问. 相关函数: CreateMutex用于 ...
- 总结windows多线程同步互斥
windows多线程同步互斥--总结 我的windows多线程系列文章: windows多线程--原子操作 windows多线程同步--事件 windows多线程同步--互斥量 windows多线程同 ...
- windows多线程同步互斥--总结
我的windows多线程系列文章: windows多线程--原子操作 windows多线程同步--事件 windows多线程同步--互斥量 windows多线程同步--临界区 windows多线程同步 ...
- [转]一个简单的Linux多线程例子 带你洞悉互斥量 信号量 条件变量编程
一个简单的Linux多线程例子 带你洞悉互斥量 信号量 条件变量编程 希望此文能给初学多线程编程的朋友带来帮助,也希望牛人多多指出错误. 另外感谢以下链接的作者给予,给我的学习带来了很大帮助 http ...
- [一个经典的多线程同步问题]解决方案三:互斥量Mutex
本篇通过互斥量来解决线程的同步,学习其中的一些知识. 互斥量也是一个内核对象,它用来确保一个线程独占一个资源的访问.互斥量与关键段的行为非常相似,并且互斥量可以用于不同进程中的线程互斥访问资源.使用互 ...
- 多线程面试题系列(7):经典线程同步 互斥量Mutex
前面介绍了关键段CS.事件Event在经典线程同步问题中的使用.本篇介绍用互斥量Mutex来解决这个问题. 互斥量也是一个内核对象,它用来确保一个线程独占一个资源的访问.互斥量与关键段的行为非常相似, ...
随机推荐
- OData WebAPI实践-OData与EDM
本文属于 OData 系列 引言 在 OData 中,EDM(Entity Data Model) 代表"实体数据模型",它是一种用于表示 Web API 中的结构化数据的格式.E ...
- 2020-11-10:golang中的接口,类型不空,值为空,如何判断是nil?
福哥答案2020-11-10: reflect.ValueOf(接口变量).IsNil(),用这个即可判断.对于值类型,会panic.两种方法如下:1.异常判断:recover捕获.2.类型判断:re ...
- Redis基础命令汇总,看这篇就够了
本文首发于公众号:Hunter后端 原文链:Redis基础命令汇总,看这篇就够了 本篇笔记将汇总 Redis 基础命令,包括几个常用的通用命令,和各个类型的数据的操作,包括字符串.哈希.列表.集合.有 ...
- Windows服务程序管理器 - 开源研究系列文章
这些天弄了一个Windows服务程序管理器,主要是对需要的Windows服务程序进行管理.这个也能够将自己开发的服务程序注册到操作系统里去运行. 1. 项目目录: 目录见下图,对代码进行 ...
- 现代 CSS 解决方案:CSS 原生支持的三角函数
在 CSS 中,存在许多数学函数,这些函数能够通过简单的计算操作来生成某些属性值,例如 : calc():用于计算任意长度.百分比或数值型数据,并将其作为 CSS 属性值. min() 和 max() ...
- 代码随想录算法训练营Day9|字符串KMP算法总结
代码随想录算法训练营 代码随想录算法训练营Day9字符串|KMP算法 8. 实现 strStr() 459.重复的子字符串 字符串总结 双指针回顾 28. 实现 strStr() KMP算法 题目链接 ...
- MYSQL数据库的创建和删除
打开Windows命令行,输入登录用户和密码 mysql -h localhost -u root -p 创建新数据 CREATE DATABASE zoo; 查看系统中的数据库 SHOW DATAB ...
- CAPL 脚本基本语句
CAPL(Communication Access Programming Language)是一种用于汽车通信网络分析和仿真的脚本语言.以下是CAPL脚本的基本语句: 1.变量声明 variable ...
- 【论文阅读】Uformer:A General U-Shaped Transformer for Image Restoration
前言 博客主页:睡晚不猿序程 首发时间:2023.6.8 最近更新时间:2023.6.8 本文由 睡晚不猿序程 原创 作者是蒻蒟本蒟,如果文章里有任何错误或者表述不清,请 tt 我,万分感谢!orz ...
- ASP.NET Core 6框架揭秘实例演示[39]:使用最简洁的代码实现登录、认证和注销
认证是一个确定请求访问者真实身份的过程,与认证相关的还有其他两个基本操作--登录和注销.ASP.NET Core利用AuthenticationMiddleware中间件完成针对请求的认证,并提供了用 ...