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来解决这个问题. 互斥量也是一个内核对象,它用来确保一个线程独占一个资源的访问.互斥量与关键段的行为非常相似, ...
随机推荐
- 关于java中的equal
正常情况下的equal方法是比较两者之间的id.如果需要它实现其他的问题,可以通过重写这个方法.idea自带了重写equal的快捷方式.右键生成中的equals() 和 hashCode()就可以帮助 ...
- IPS 和 IDS
IPS/IDS 什么是IPS和IDS IDS/IPS是检测和防止对网络服务器进行未授权的访问的系统.有许多产品同时有IDS和IPS的作用,作为加强企业信息安全所必须的系统 什么是IDS(Intrusi ...
- 2021-07-09:股票问题6。给定一个整数数组 prices,其中第 i 个元素代表了第 i 天的股票价格 ;整数 fee 代表了交易股票的手续费用。你可以无限次地完成交易,但是你每笔交易都需要付
2021-07-09:股票问题6.给定一个整数数组 prices,其中第 i 个元素代表了第 i 天的股票价格 :整数 fee 代表了交易股票的手续费用.你可以无限次地完成交易,但是你每笔交易都需要付 ...
- 2021-12-21:任务调度器。 给你一个用字符数组 tasks 表示的 CPU 需要执行的任务列表。其中每个字母表示一种不同种类的任务。任务可以以任意顺序执行,并且每个任务都可以在 1 个单位时间
2021-12-21:任务调度器. 给你一个用字符数组 tasks 表示的 CPU 需要执行的任务列表.其中每个字母表示一种不同种类的任务.任务可以以任意顺序执行,并且每个任务都可以在 1 个单位时间 ...
- vue全家桶进阶之路7:Vue的第一个程序
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8" ...
- PTA L1-064 估值一亿的AI核心代码
PTA L1-064 估值一亿的AI核心代码 有坑!不少 题目链接 题目及分析 题目: 本题要求你实现一个稍微更值钱一点的 AI 英文问答程序,规则是: 1. 无论用户说什么,首先把对方说 ...
- [SWPUCTF 2021 新生赛]no_wakeup
[SWPUCTF 2021 新生赛]no_wakeup 考点 反序列化 一.题目 打开题目发现如下代码 <?php header("Content-type:text/html;cha ...
- ODOO页面使用css和js的流程
1 首先定义页面 <data> <record id="myquality_iqcbasesetup_form" model="ir.ui.view&q ...
- [Docker] Docker之安装Nginx
0 序言 略 1 安装步骤 Step1 下载镜像 搜素.下载镜像 https://hub.docker.com/_/nginx?tab=tags 这里选择官方镜像1.22.0版本 docker sea ...
- go 实现ringbuffer以及ringbuffer使用场景介绍
ringbuffer因为它能复用缓冲空间,通常用于网络通信连接的读写,虽然市面上已经有了go写的诸多版本的ringbuffer组件,虽然诸多版本,实现ringbuffer的核心逻辑却是不变的.但发现其 ...