2024.4.9记——C++多线程系列文章(五)之死锁

引言

如果用单一的全局互斥保护所有共享数据,也即锁的粒度过大,例如在共享大量数据的系统中,这么做会消除并发带来的任何性能优势,原因是多线程系统由此受到强制限定,任意时刻都只准许运行其中一个线程,即便它们访问不同的数据。

另一方面如果锁的粒度太小,需要被保护的操作没有被完整覆盖,就会出现接口的恶性条件竞争。精细粒度的加锁策略也存在问题。为了保护同一个操作涉及的所有数据,我们有时候需要锁住多个互斥。假如我们终须针对某项操作锁住多个互斥,那就会让一个问题藏匿起来,伺机进行扰乱:死锁。条件竞争是两个线程同时抢先运行,死锁则差不多是其反面:两个线程同时互相等待,停滞不前。

产生的原因

假设一件玩具由两部分组成,需要同时配合才能玩,譬如玩具鼓和鼓槌;又假设有两个小孩都喜欢这件玩具。倘若其中一个孩子拿到了玩具鼓和鼓槌,那他就能尽兴地一直敲鼓,敲烦了才停止。如果另一个孩子也想玩,便须等待,即使感到难受也没办法。再想象一下,玩具鼓和鼓槌分散在玩具箱里,两个小孩同时都想玩。于是,他们翻遍玩具箱,其中一人找到了玩具鼓,另一人找到了鼓槌,除非当中一位割爱让对方先玩,否则,他们只会僵持不下,各自都紧抓手中的部件不放,还要求对方“缴械”,结果都玩不成。

现在进行类比,我们面对的并非小孩争抢玩具,而是线程在互斥上争抢锁:有两个线程,都需要同时锁住两个互斥,才可以进行某项操作,但它们分别都只锁住了一个互斥,都等着再给另一个互斥加锁。于是,双方毫无进展,因为它们同在苦苦等待对方解锁互斥。上述情形称为死锁(deadlock)。为了进行某项操作而对多个互斥加锁,由此诱发的最大的问题之一正是死锁。

解决方法

防范死锁的建议通常是,始终按相同顺序对两个互斥加锁。若我们总是先锁互斥A再锁互斥B,则永远不会发生死锁。有时候,这直观、易懂,因为诸多互斥的用途各异。但也会出现棘手的状况,例如,运用多个互斥分别保护多个独立的实例,这些实例属于同一个类。考虑一个函数,其操作同一个类的两个实例,互相交换它们的内部数据。为了保证互换正确完成,免受并发改动的不良影响,两个实例上的互斥都必须加锁。可是,如果选用了固定的次序(两个对象通过参数传入,我们总是先给第一个实例的互斥加锁,再轮到第二个实例的互斥),前面的建议就适得其反:针对两个相同的实例,若两个线程都通过该函数在它们之间互换数据,只是两次调用的参数顺序相反,会导致它们陷入死锁!

所幸,C++标准库提供了 std::lock() 函数,专门解决这一问题。它可以同时锁住多个互斥,而没有发生死锁的风险。以下代码给出了示范,在简单的内部数据互换操作中运用std::lock()函数。

class some_big_object;
void swap(some_big_object& lhs, some_big_object& rhs);
class X
{
private:some_big_object some_detail;std::mutex m;
public:X(some_big_object const& sd):some_detail(sd){}friend void swap(X& lhs, X& rhs){if(&lhs==&rhs)return;std::lock(lhs.m,rhs.m);    // ⇽---  ①std::lock_guard<std::mutex> lock_a(lhs.m, std::adopt_lock);   // ⇽---  ②std::lock_guard<std::mutex> lock_b(rhs.m, std::adopt_lock);   // ⇽---  ③swap(lhs.some_detail,rhs.some_detail);}
};

本例的函数一开始就对比两个参数,以确定它们指向不同实例。此项判断必不可少,原因是,若我们已经在某个std::mutex对象上获取锁,那么再次试图从该互斥获取锁将导致未定义行为(std::recursive_mutex类型的互斥准许同一线程重复加锁,后续介绍)。接着,代码调用std::lock()锁定两个互斥①,并依据它们分别构造std::lock_guard实例②③。我们除了用互斥充当这两个实例的构造参数,还额外提供了 std::adopt_lock 对象,以指明互斥已被锁住,即互斥上有锁存在,std::lock_guard实例应当据此接收锁的归属权,不得在构造函数内试图另行加锁。

无论函数是正常返回,还是因受保护的操作抛出异常而导致退出,std::lock_guard都保证了互斥全都正确解锁。另外,值得注意的是,std::lock()在其内部对lhs.m或rhs.m加锁,这一函数调用可能导致抛出异常,这样,异常便会从std::lock()向外传播。假如std::lock()函数在其中一个互斥上成功获取了锁,但它试图在另一个互斥上获取锁时却有异常抛出,那么第一个锁就会自动释放:若加锁操作涉及多个互斥,则std::lock()函数的语义是“全员共同成败”(all-or-nothing,或全部成功锁定,或没获取任何锁并抛出异常)。

针对上述场景,C++17还进一步提供了新的RAII类模板std::scoped_lock<>。std:: scoped_lock<>和std::lock_guard<>完全等价,只不过前者是可变参数模板(variadic template),接收各种互斥型别作为模板参数列表,还以多个互斥对象作为构造函数的参数列表。下列代码中,传入构造函数的两个互斥都被加锁,机制与std::lock()函数相同,因此,当构造函数完成时,它们就都被锁定,而后,在析构函数内一起被解锁。我们可以重写上述代码中的swap()函数,其内部操作代码如下:

void swap(X& lhs, X& rhs)
{if(&lhs==&rhs)return;std::scoped_lock guard(lhs.m,rhs.m);    // ⇽---  ①swap(lhs.some_detail,rhs.some_detail);
}

上例利用了C++17加入的另一个新特性:类模板参数推导。假使读者的编译器支持C++17标准(通过查验能否使用std::scoped_lock即可判断,因为它是C++17程序库工具),C++17具有隐式类模板参数推导(implicit class template parameter deduction)机制,依据传入构造函数的参数对象自动匹配①,选择正确的互斥型别。①处的语句等价于下面完整写明的版本:

std::scoped_lock<std::mutex,std::mutex> guard(lhs.m,rhs.m);

在C++17之前,我们采用std::lock()编写代码。现在有了std::scoped_lock,于是那些代码绝大多数可以改用这个编写,从而降低出错的概率。这肯定是件好事!

假定我们需要同时获取多个锁,那么std::lock()函数和std::scoped_lock<>模板即可帮助防范死锁;但若代码分别获取各个锁,它们就鞭长莫及了。在这种情况下,唯有依靠经验力求防范死锁。知易行难,死锁是最棘手的多线程代码问题之一,绝大多数情形中,纵然一切都运作正常,死锁也往往无法预测。尽管如此,我们编写的代码只要服从一些相对简单的规则,便有助于防范死锁。

防范死锁的补充准则

虽然死锁的最常见诱因之一是锁操作,但即使没有牵涉锁,也会发生死锁现象。假定有两个线程,各自关联了std::thread实例,若它们同时在对方的std::thread实例上调用join(),就能制造出死锁现象却不涉及锁操作。这种情形与前文的小孩争抢玩具相似,两个线程都因苦等对方结束而停滞不前。如果线程甲正等待线程乙完成某一动作,同时线程乙却在等待线程甲完成某一动作,便会构成简单的循环等待,并且线程数目不限于两个:就算是3个或更多线程,照样会引起死锁。防范死锁的准则最终可归纳成一个思想:只要另一线程有可能正在等待当前线程,那么当前线程千万不能反过来等待它。下列准则的细分条目给出了各种方法,用于判别和排除其他线程是否正在等待当前线程。

1.避免嵌套锁

第一条准则最简单:假如已经持有锁,就不要试图获取第二个锁。若能恪守这点,每个线程便最多只能持有唯一一个锁,仅锁的使用本身不可能导致死锁。但是还存在其他可能引起死锁的场景(譬如,多个线程彼此等待),而操作多个互斥锁很可能就是最常见的死锁诱因。万一确有需要获取多个锁,我们应采用std::lock()函数,借单独的调用动作一次获取全部锁来避免死锁。

2.一旦持锁,就须避免调用由用户提供的程序接口

这是上一条准则的延伸。若程序接口由用户自行实现,则我们无从得知它到底会做什么,它可能会随意操作,包括试图获取锁。一旦我们已经持锁,若再调用由用户提供的程序接口,而它恰好也要获取锁,那便违反了避免嵌套锁的准则,可能发生死锁。不过,有时这实在难以避免。对于类似3.2.3节的栈容器的泛型代码,只要其操作与模板参数的型别有关,它就不得不依赖用户提供的程序接口。因此我们需要另一条新的准则。

3.依从固定顺序获取锁

如果多个锁是绝对必要的,却无法通过std::lock()在一步操作中全部获取,我们只能退而求其次,在每个线程内部都依从固定顺序获取这些锁。若在两个互斥上获取锁,则有办法防范死锁:关键是,事先规定好加锁顺序,令所有线程都依从。这在一些情况下相对简单、易行。虽然这种方式并不一定总是可行,但在该情况下,我们至少可以同时对两个互斥加锁(上面例子)。

4.按层级加锁

锁的层级划分就是按特定方式规定加锁次序,在运行期据此查验加锁操作是否遵从预设规则。按照构思,我们把应用程序分层,并且明确每个互斥位于哪个层级。若某线程已对低层级互斥加锁,则不准它再对高层级互斥加锁。具体做法是将层级的编号赋予对应层级应用程序上的互斥,并记录各线程分别锁定了哪些互斥。这种模式虽然常见,但C++标准库尚未提供直接支持。

5.将准则推广到锁操作以外

之前就提到过,死锁现象并不单单因加锁操作而发生,任何同步机制导致的循环等待都会导致死锁出现。因此也值得为那些情况推广上述准则。譬如,我们应尽可能避免获取嵌套锁;若当前线程持有某个锁,却又同时等待别的线程,这便是坏的情况,因为万一后者恰好也需获取锁,反而只能等该锁被释放才能继续运行。类似地,如果要等待线程,那就值得针对线程规定层级,使得每个线程仅等待层级更低的线程。有一种简单方法可实现这种机制:让同一个函数启动全部线程,且汇合工作也由之负责。只要代码采取了防范死锁的设计,函数std::lock()和类std::lock_guard即可涵盖大多数简单的锁操作,不过我们有时需要更加灵活。标准库针对一些情况提供了std::unique_lock<>模板。它与std::lock_guard<>一样,也是一个依据互斥作为参数的类模板,并且以RAII手法管理锁,不过它更灵活一些,我们将在后续文章一一介绍。

前面文章

2024.3.27记——C++多线程系列文章(一)
2024.3.28记——C++多线程系列文章(二)之向线程函数传递参数
2024.3.29记——C++多线程系列文章(三)线程归属权转移及线程识别
2024.4.1记——C++多线程系列文章(四)之互斥

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/802089.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

python应援灯牌代码

代码如下 import pygame import sys import random# 初始化Pygame pygame.init()# 设置窗口尺寸 WINDOW_WIDTH 800 WINDOW_HEIGHT 600 window_surface pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT)) pygame.display.set_caption(应援语跑马灯模拟)# 定义颜色 WH…

Web API(六)之正则表达式

Web API(六)之正则表达式 正则表达式正则基本使用元字符边界符量词范围字符类替换和修饰符change 事件判断是否有类正则表达式 正则表达式(Regular Expression)是一种字符串匹配的模式(规则) 使用场景: 例如验证表单:手机号表单要求用户只能输入11位的数字 (匹配)过滤…

利用AI开源引擎:文本自动摘要提取API (可本地化部署)

新闻摘要提取技术是一种高效的自然语言处理技术&#xff0c;它通过算法对新闻文章进行全面的语义理解和分析&#xff0c;自动抽取文章中的关键信息&#xff0c;生成简洁、连贯的摘要文本。该技术在多个领域都有着广泛的应用&#xff0c;下面将深入探讨其主要的应用场景。 开源项…

【接口】HTTP(4)|Session、Cookie、token有什么区别?

HTTP是一种无状态的协议&#xff0c;服务器本身没有记忆功能&#xff0c;无法记住客户端之前是否有发送过请求&#xff0c;因此客户端每次发送的请求都是独立的 无状态&#xff1a;HTTP是一个无状态的协议。这句话里的无状态是什么意思&#xff1f; - 知乎 1、为什么需要cookie…

Rust通用代码生成器莲花,红莲尝鲜版二十二,发布数据库自动反射新功能及其视频

Rust通用代码生成器莲花&#xff0c;红莲尝鲜版二十二&#xff0c;发布数据库自动反射新功能及其视频 Rust 通用代码生成器莲花&#xff0c;红莲尝鲜版二十二&#xff0c;此版本新增了数据库自动反射功能&#xff0c;可以为遗留数据库配上操作软件。此版本增强了模板向导界面的…

LINUX 下IPTABLES配置详解

-t<表>&#xff1a;指定要操纵的表&#xff1b; -A&#xff1a;向规则链中添加条目&#xff1b; -D&#xff1a;从规则链中删除条目&#xff1b; -i&#xff1a;向规则链中插入条目&#xff1b; -R&#xff1a;替换规则链中的条目&#xff1b; -L&#xff1a;显示规则链中…

MQ中的交换机与队列

交换机与队列的介绍 1.流程 首先先介绍一个简单的一个消息推送到接收的流程&#xff0c;提供一个简单的图 黄色的圈圈就是我们的消息推送服务&#xff0c;将消息推送到 中间方框里面也就是 rabbitMq的服务器&#xff0c;然后经过服务器里面的交换机、队列等各种关系&#xff…

最简单知识点PyTorch中的nn.Linear(1, 1)

一、nn.Linear(1, 1) nn.Linear(1, 1) 是 PyTorch 中的一个线性层&#xff08;全连接层&#xff09;的定义。 nn 是 PyTorch 的神经网络模块&#xff08;torch.nn&#xff09;的常用缩写。 nn.Linear(1, 1) 的含义如下&#xff1a; 第一个参数 1&#xff1a;输入特征的数量…

【人工智能】AI赋能城市交通 未来城市的驱动力

前言 随着城市化进程的不断加速&#xff0c;交通拥堵、环境污染等问题日益凸显&#xff0c;人们对交通系统的效率和可持续性提出了更高的要求。在这样的背景下&#xff0c;智能交通技术正成为改善城市交通的重要驱动力。本文将探讨智能交通技术在解决城市交通挑战方面的应用和未…

为什么要在 Golang 中使用指针

1、性能优化&#xff1a;指针允许你在不复制数据的情况下访问和修改变量。当你有一个大型的数据结构时&#xff0c;如果直接传递这个数据结构&#xff0c;会产生一个完整的副本&#xff0c;这会增加内存的使用和CPU的计算时间。使用指针&#xff0c;你可以只传递数据的地址&…

谷歌留痕霸屏要怎么做?

谷歌留痕霸屏&#xff0c;就是让你的网站或者页面在谷歌搜索结果里尽可能多地出现&#xff0c;就像是在你的潜在客户眼前留下深刻印象一样&#xff0c;你要做的就是在一些高权重平台发布有价值的信息&#xff0c;同时巧妙地留下你的品牌名、产品名或者任何你想要推广的关键词&a…

css实现各级标题自动编号

本文在博客同步发布&#xff0c;您也可以在这里看到最新的文章 Markdown编辑器大多不会提供分级标题的自动编号功能&#xff0c;但我们可以通过简单的css样式设置实现。 本文介绍了使用css实现各级标题自动编号的方法&#xff0c;本方法同样适用于typora编辑器和wordpress主题…

六角螺母缺陷分类数据集:3440张图像

六角螺母缺陷数据集&#xff1a;包含变形&#xff0c;划痕&#xff0c;断裂&#xff0c;生锈&#xff0c;以及优质螺母图片数据&#xff0c;共计3440张&#xff0c;无标注 一.变形螺母-1839 二.断裂螺母-287 三.划痕螺母-473 四.生锈螺母-529 五.优良螺母-312 适用于CV项目&am…

Flutter之Flex组件布局

目录 Flex属性值 轴向:direction:Axis.horizontal 主轴方向:mainAxisAlignment:MainAxisAlignment.center 交叉轴方向:crossAxisAlignment:CrossAxisAlignment 主轴尺寸:mainAxisSize 文字方向:textDirection:TextDirection 竖直方向排序:verticalDirection:VerticalDir…

灵猫论文好用吗 #媒体#笔记

灵猫论文是一款专门用于论文写作、查重降重的工具&#xff0c;它的使用方便、高效&#xff0c;深受广大论文作者的喜爱。那么&#xff0c;灵猫论文到底好用吗&#xff1f;答案是肯定的&#xff01; 首先&#xff0c;灵猫论文提供了强大的查重降重功能&#xff0c;能够帮助用户快…

MySQL8.0新特性详解及全局优化

文章目录 一、前言二、开窗函数三、新增函数索引四、group by不再隐式排序五、新增降序索引六、binlog日志文件过期时间精确到秒七、undo文件不再使用系统表空间八、默认字符集由latin1变为utf8mb4九、自增变量持久化十、删除了.frm等文件 一、前言 目前MySQL8.0及以上版本在我…

Commitizen:规范化你的 Git 提交信息

简介 在团队协作开发过程中&#xff0c;规范化的 Git 提交信息可以提高代码维护的效率&#xff0c;便于追踪和定位问题。Commitizen 是一个帮助我们规范化 Git 提交信息的工具&#xff0c;它提供了一种交互式的方式来生成符合约定格式的提交信息。 原理 Commitizen 的核心原…

Vue3跟Vue2比,性能真的有所提升吗?

答案是肯定的。 说起Vue3的改进&#xff0c;很多人都会说出响应式的改变&#xff0c;与Vue2相比&#xff0c;Vue3采用了proxy的方式对响应式做了重写&#xff0c;而Vue2则是采用defineProperty的方式将对象的属性进行深度遍历&#xff0c;而这种方式想要实现响应式的前与后&am…

Python random randint() 方法

Python random randint() 方法 Python random 模块 Python random 模块 Python random.randint() 方法返回指定范围内的整数。 randint(start, stop) 等价于 randrange(start, stop1)。 语法 random.randint() 方法语法如下&#xff1a; random.randint(start, stop) 参数…

C++:万能进制转换

目录 引言一、p进制转为十进制二、十进制转换为p进制三、测试 引言 本文介绍的这个 p p p 进制与十进制之间的转换&#xff0c;主要是针对算法竞赛中的&#xff0c;对于一些特殊情况&#xff0c;比如说字母大小写、前导零、非法输入的处理等这些问题其实都没有去深究&#xf…