为什么多线程读写 shared_ptr 要加锁?

news2024/11/29 3:45:50

陈硕(giantchen_AT_gmail_DOT_com)

2012-01-28

我在《Linux 多线程服务端编程:使用 muduo C++ 网络库》第 1.9 节“再论 shared_ptr 的线程安全”中写道:

(shared_ptr)的引用计数本身是安全且无锁的,但对象的读写则不是,因为 shared_ptr 有两个数据成员,读写操作不能原子化。根据文档(Boost.SmartPtr: The Smart Pointer Library - 1.81.0), shared_ptr 的线程安全级别和内建类型、标准库容器、std::string 一样,即:

• 一个 shared_ptr 对象实体可被多个线程同时读取(文档例1);

• 两个 shared_ptr 对象实体可以被两个线程同时写入(例2),“析构”算写操作;

• 如果要从多个线程读写同一个 shared_ptr 对象,那么需要加锁(例3~5)。

请注意,以上是 shared_ptr 对象本身的线程安全级别,不是它管理的对象的线程安全级别。

后文(p.18)则介绍如何高效地加锁解锁。本文则具体分析一下为什么“因为 shared_ptr 有两个数据成员,读写操作不能原子化”使得多线程读写同一个 shared_ptr 对象需要加锁。这个在我看来显而易见的结论似乎也有人抱有疑问,那将导致灾难性的后果,值得我写这篇文章。本文以 boost::shared_ptr 为例,与 std::shared_ptr 可能略有区别。

shared_ptr 的数据结构

shared_ptr 是引用计数型(reference counting)智能指针,几乎所有的实现都采用在堆(heap)上放个计数值(count)的办法(除此之外理论上还有用循环链表的办法,不过没有实例)。具体来说,shared_ptr<Foo> 包含两个成员,一个是指向 Foo 的指针 ptr,另一个是 ref_count 指针(其类型不一定是原始指针,有可能是 class 类型,但不影响这里的讨论),指向堆上的 ref_count 对象。ref_count 对象有多个成员,具体的数据结构如图 1 所示,其中 deleter 和 allocator 是可选的。

图 1:shared_ptr 的数据结构。

为了简化并突出重点,后文只画出 use_count 的值:

以上是 shared_ptr<Foo> x(new Foo); 对应的内存数据结构。

如果再执行 shared_ptr<Foo> y = x; 那么对应的数据结构如下。

但是 y=x 涉及两个成员的复制,这两步拷贝不会同时(原子)发生。

中间步骤 1,复制 ptr 指针:

中间步骤 2,复制 ref_count 指针,导致引用计数加 1:

步骤1和步骤2的先后顺序跟实现相关(因此步骤 2 里没有画出 y.ptr 的指向),我见过的都是先1后2。

既然 y=x 有两个步骤,如果没有 mutex 保护,那么在多线程里就有 race condition。

多线程无保护读写 shared_ptr 可能出现的 race condition

考虑一个简单的场景,有 3 个 shared_ptr<Foo> 对象 x、g、n:

  • shared_ptr<Foo> g(new Foo); // 线程之间共享的 shared_ptr
  • shared_ptr<Foo> x; // 线程 A 的局部变量
  • shared_ptr<Foo> n(new Foo); // 线程 B 的局部变量

一开始,各安其事。

线程 A 执行 x = g; (即 read g),以下完成了步骤 1,还没来及执行步骤 2。这时切换到了 B 线程。

同时编程 B 执行 g = n; (即 write g),两个步骤一起完成了。

先是步骤 1:

再是步骤 2:

这是 Foo1 对象已经销毁,x.ptr 成了空悬指针!

最后回到线程 A,完成步骤 2:

多线程无保护地读写 g,造成了“x 是空悬指针”的后果。这正是多线程读写同一个 shared_ptr 必须加锁的原因。

当然,race condition 远不止这一种,其他线程交织(interweaving)有可能会造成其他错误。

思考,假如 shared_ptr 的 operator= 实现是先复制 ref_count(步骤 2)再复制 ptr(步骤 1),会有哪些 race condition?

杂项

shared_ptr 作为 unordered_map 的 key

如果把 boost::shared_ptr 放到 unordered_set 中,或者用于 unordered_map 的 key,那么要小心 hash table 退化为链表。boost - C++: shared_ptr as unordered_set's key - Stack Overflow

直到 Boost 1.47.0 发布之前,unordered_set<std::shared_ptr<T> > 虽然可以编译通过,但是其 hash_value 是 shared_ptr 隐式转换为 bool 的结果。也就是说,如果不自定义hash函数,那么 unordered_{set/map} 会退化为链表。https://svn.boost.org/trac/boost/ticket/5216

Boost 1.51 在 boost/functional/hash/extensions.hpp 中增加了有关重载,现在只要包含这个头文件就能安全高效地使用 unordered_set<std::shared_ptr> 了。

这也是 muduo 的 examples/idleconnection 示例要自己定义 hash_value(const boost::shared_ptr<T>& x) 函数的原因(书第 7.10.2 节,p.255)。因为 Debian 6 Squeeze、Ubuntu 10.04 LTS 里的 boost 版本都有这个 bug。

为什么图 1 中的 ref_count 也有指向 Foo 的指针?

shared_ptr<Foo> sp(new Foo) 在构造 sp 的时候捕获了 Foo 的析构行为。实际上 shared_ptr.ptr 和 ref_count.ptr 可以是不同的类型(只要它们之间存在隐式转换),这是 shared_ptr 的一大功能。分 3 点来说:

1. 无需虚析构;假设 Bar 是 Foo 的基类,但是 Bar 和 Foo 都没有虚析构。

shared_ptr<Foo> sp1(new Foo); // ref_count.ptr 的类型是 Foo*

shared_ptr<Bar> sp2 = sp1; // 可以赋值,自动向上转型(up-cast)

sp1.reset(); // 这时 Foo 对象的引用计数降为 1

此后 sp2 仍然能安全地管理 Foo 对象的生命期,并安全完整地释放 Foo,因为其 ref_count 记住了 Foo 的实际类型。

2. shared_ptr<void> 可以指向并安全地管理(析构或防止析构)任何对象;muduo::net::Channel class 的 tie() 函数就使用了这一特性,防止对象过早析构,见书 7.15.3 节。

shared_ptr<Foo> sp1(new Foo); // ref_count.ptr 的类型是 Foo*

shared_ptr<void> sp2 = sp1; // 可以赋值,Foo* 向 void* 自动转型

sp1.reset(); // 这时 Foo 对象的引用计数降为 1

此后 sp2 仍然能安全地管理 Foo 对象的生命期,并安全完整地释放 Foo,不会出现 delete void* 的情况,因为 delete 的是 ref_count.ptr,不是 sp2.ptr。

3. 多继承。假设 Bar 是 Foo 的多个基类之一,那么:

shared_ptr<Foo> sp1(new Foo);

shared_ptr<Bar> sp2 = sp1; // 这时 sp1.ptr 和 sp2.ptr 可能指向不同的地址,因为 Bar subobject 在 Foo object 中的 offset 可能不为0。

sp1.reset(); // 此时 Foo 对象的引用计数降为 1

但是 sp2 仍然能安全地管理 Foo 对象的生命期,并安全完整地释放 Foo,因为 delete 的不是 Bar*,而是原来的 Foo*。换句话说,sp2.ptr 和 ref_count.ptr 可能具有不同的值(当然它们的类型也不同)。

为什么要尽量使用 make_shared()?

为了节省一次内存分配,原来 shared_ptr<Foo> x(new Foo); 需要为 Foo 和 ref_count 各分配一次内存,现在用 make_shared() 的话,可以一次分配一块足够大的内存,供 Foo 和 ref_count 对象容身。数据结构是:

不过 Foo 的构造函数参数要传给 make_shared(),后者再传给 Foo::Foo(),这只有在 C++11 里通过 perfect forwarding 才能完美解决。

参考:

https://www.cnblogs.com/Solstice/archive/2013/01/28/2879366.htmlicon-default.png?t=N2N8https://www.cnblogs.com/Solstice/archive/2013/01/28/2879366.html

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

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

相关文章

git仓库与分支

仓库使用 第一次进入需要创建一个仓库 按照教程绑定邮箱可密码&#xff0c;再创建readme文件上传到仓库初始化 git push 详解 git push origin master:xiaoxu# 如下为远程仓库的详解https://gitee.com/fireapproval/xiaoxu.git//其中xiaoxu为仓库//如上的可视化界面中bolen为…

伺服阀放大器接线设置

端子 功能描述 1 PWR 外部电源输入&#xff0c;24VDC 2 PWR- 外部电源输入&#xff0c;24VDC 3 ENA 低电平使能 0<U<1.5V&#xff0c;放大器工作&#xff0c;绿灯常亮&#xff1b;2.5V<U<UB&#xff0c;放大器不工作&#xff0c;红灯常亮&#xff1b; 4 N.C. 不…

【关于Linux中----信号量及其使用场景】

文章目录一、解释信号量1.1 概念的引入1.2 信号量操作和使用接口二、信号量使用场景2.1 引入环形队列&&生产消费问题2.2 代码实现2.3 对于多生产多消费的情况2.4 申请信号量和加锁的顺序问题2.5 多生产多消费的意义一、解释信号量 1.1 概念的引入 我们知道&#xff0…

python uiautomator2 环境搭建和基本使用

安装 adb安装和配置 可以百度或者看看这个https://blog.csdn.net/weixin_37600187/article/details/127987889 安装uiautomator2 pip install -i https://mirrors.aliyun.com/pypi/simple -U uiautomator2安装 weditor&#xff08;一般情况会报错&#xff09; pip install…

C#程序设计——面向对象编程基础,设计一个Windows应用程序,模拟一个简单的银行账户管理系统。实现创建账户、取款、存款和查询余额的模拟操作。

一、实验目的 1、理解简单程序中的面向对象思想的应用 2、掌握C#的定义类和创建对象的方法 3、理解方法中参数的传递 二、实验内容 1、设计一个Windows应用程序&#xff0c;模拟一个简单的银行账户管理系统。实现创建账户、取款、存款和查询余额的模拟操…

序列比对算法

案例问题&#xff1a;假设有两个序列&#xff1a;ATGCG 和 ACCG&#xff0c;如何求得它们的最佳匹配方案。 1. Needleman-Wunsch 算法 原理是动态规划&#xff0c;是一个全局比对算法 算法求解步骤&#xff1a; &#xff08;1&#xff09;在它们前面各加上一个 ‘-’ -ATGC…

2023 “认证杯”数学中国数学建模C 题 心脏危险事件 详细思路

2023年认证杯”数学中国数学建模如期开赛&#xff0c;本次比赛与妈杯&#xff0c;泰迪杯时间有点冲突。因此&#xff0c;个人精力有限&#xff0c;有些不可避免地错误欢迎大家指出。为了大家更方便的选题&#xff0c;我将为大家带来C题的详细解析&#xff0c;以方便大家建模分析…

如何有效利用文旅资源

文旅产业是当今世界发展最迅速的行业之一&#xff0c;文化和旅游业的融合已经成为文旅产业发展的趋势。众所周知&#xff0c;文旅资源是我国的宝贵财富&#xff0c;文化遗产、旅游胜地等都是国宝级的文旅资源&#xff0c;从古老的文化遗产到现代的旅游景点&#xff0c;无不体现…

近期CTF web

文章目录NKCTFbaby_phpez_phphard_phpeasy_pmseasy_cmsWebPageTestxiaopiCTFshow愚人赛easy_signineasy_sstiez_flask被遗忘的反序列化easy_php杭师大CTFfindmeez_javaeznodeNKCTF baby_php <?phperror_reporting(0);class Welcome{public $name;public $arg oww!man!!;…

【八】springboot启动源码 - finishRefresh

Last step: publish corresponding event. clearResourceCaches initLifecycleProcessor Initialize lifecycle processor for this context. 从IOC获取LifecycleProcessor设置到applicationContext中,如果从IOC中获取不到会创建DefaultLifecycleProcessor并注册到IOC中 get…

【能力提升】SQL Server常见问题介绍及快速解决建议

前言 本文旨在帮助SQL Server数据库的使用人员了解常见的问题&#xff0c;及快速解决这些问题。这些问题是数据库的常规管理问题&#xff0c;对于很多对数据库没有深入了解的朋友提供一个大概的常见问题框架。 下面一些问题是在近千家数据库用户诊断时发现的常规问题&#xff0…

用Abp实现找回密码和密码强制过期策略

文章目录重置密码找回密码发送验证码校验验证码发送重置密码链接创建接口密码强制过期策略改写接口Vue网页端开发重置密码页面忘记密码控件密码过期提示项目地址用户找回密码&#xff0c;确切地说是 重置密码&#xff0c;为了保证用户账号安全&#xff0c;原始密码将不再以明文…

一篇文章搞定《动手学深度学习》-(李牧)PyTorch版本的所有内容

目录 目录 简介 阅读指南 1. 深度学习简介 2. 预备知识 3. 深度学习基础 4. 深度学习计算 5. 卷积神经网络 6. 循环神经网络 7. 优化算法 8. 计算性能 9. 计算机视觉 10. 自然语言处理 环境 参考&#xff08;大家可以在这里下载代码&#xff09; 原书地址&#…

优思学院|精益生产和精益管理的区别

精益生产和精益管理&#xff0c;这两个概念我们或多或少都听说过。但是&#xff0c;你是否真的明白这两个概念的区别&#xff1f;或者你是否也像我一样&#xff0c;之前把这两个概念混淆在一起呢&#xff1f;今天&#xff0c;我要和大家分享的是&#xff0c;精益生产和精益管理…

用Flutter开发一款音乐App(从0到1开发一款音乐App)

Flutter Music_Listener(flutter音乐播放器) Flutter version 3.9 项目介绍 1、项目整体基于getxretrofitdiojsonserialize开发 2、封装通用控制器BaseController&#xff0c;类似jetpack mvvm框架中的BaseViemodel 3、封装基础无状态基类BaseStatelessWidget&#xff0c;结合…

jmap执行失败了,怎么获取heapdump?

在之前的OOM问题复盘中&#xff0c;我们添加了jmap脚本来自动dump内存现场&#xff0c;方便排查OOM问题。 但当我反复模拟OOM场景测试时&#xff0c;发现jmap有时可以dump成功&#xff0c;有时会报错&#xff0c;如下&#xff1a; 经过网上一顿搜索&#xff0c;发现两种原因可…

来 Azure 学习 OpenAI 三 - 用 Python 调用 Azure OpenAi API

大家好&#xff0c;我是微软学生大使 Jambo。在我们申请好 Azure 和 Azure OpenAI 之后&#xff0c;我们就可以开始使用 OpenAI 模型了。如果你还没有申请 Azure 和 Azure OpenAI&#xff0c;可以参考 注册 Azure 和申请 OpenAI。 本文将会以 Azure 提供的 Openai 端口为例&am…

2023年4月广东省计算机软考中/高级备考班招生简章

软考是全国计算机技术与软件专业技术资格&#xff08;水平&#xff09;考试&#xff08;简称软考&#xff09;项目&#xff0c;是由国家人力资源和社会保障部、工业和信息化部共同组织的国家级考试&#xff0c;既属于国家职业资格考试&#xff0c;又是职称资格考试。 系统集成…

VS Code 插件开发概览

VS Code 插件开发概览 前言 VS Code作为开发者的代码开发利器&#xff0c;越来越受开发者的喜爱。像我身边的前端&#xff0c;每天80%的开发工作都是在VS Code上完成的。随着人们对它的使用&#xff0c;不再满足简单的优雅&#xff0c;舒服写代码这一基本需求。有些人利用它进…

FA-PEG-MAL,叶酸-聚乙二醇-马来酰亚胺 实验用科研试剂;Folic acid PEG Maleimide

FA-PEG-MAL,叶酸-聚乙二醇-马来酰亚胺 中文名称&#xff1a;叶酸-聚乙二醇-马来酰亚胺 英文名称&#xff1a;Folic acid PEG Maleimide, FA-PEG-MAL 性状&#xff1a;固体或者粘稠液体&#xff0c;取决于分子量大小。 溶剂&#xff1a;溶于水、DMF、DMSO等常规有机溶剂 分…