C++的模板(十):shared_ptr的上锁问题

C++STL中的智能指针shared_ptr以前没用过,它是不是线程安全过去也没关注过。很多说它是不安全的,也有说是安全的。线程安全的问题,简单测试是测不出,到底怎么样,需要直接看代码。

从代码看,shared_ptr是个简单包装,真正的代码从__shared_ptr开始。shared_ptr不过是换了个名字,并从__shared_ptr引出构造函数和赋值运算:

  template<typename _Tp>class shared_ptr: public __shared_ptr<_Tp>{};

__shared_ptr含有2个数据元素:

  template<typename _Tp>class __shared_ptr {_Tp*         	   _M_ptr;         __shared_count<_Lp>  _M_refcount;   };

_M_ptr是被__shared_ptr管理的对象指针,_M_refcount又是一个简单包装,内部包含一个指向_Sp_counted_base的指针:

  template<_Lock_policy _Lp = __default_lock_policy>class __shared_count{_Sp_counted_base<_Lp>*  _M_pi;};

这是个多态指针,实际使用时还要扩展一个指针成员:

  template<typename _Ptr, _Lock_policy _Lp>class _Sp_counted_ptr: public _Sp_counted_base<_Lp>{_Ptr             _M_ptr;  // .... ........... .... ... .....};

这个_M_ptr也是那个被引用计数管理的对象指针,在_Sp_counted_base及其派生类的管理范围内,如果引用计数归0,最后要销毁这个被管理的对象。但回溯到__shared_ptr 类去找到这个_M_ptr,增加了难度,所以这里又保存了一次。反过来看,这里保存的是真本,而__shared_ptr 类保存的是为了优化指针运算符重载而保留的副本。

基础类_Sp_counted_base除了扩展一个指针,根据需要,还可以扩展自定义分配器和清除器。并非所有的对象直接从new出来,有的通过自定义内存管理分配,它们会有自己专用的分配器和清除器:

  template<typename _Ptr, typename _Deleter, typename _Alloc, _Lock_policy _Lp>class _Sp_counted_deleter: public _Sp_counted_ptr<_Ptr, _Lp>{typedef typename _Alloc::templaterebind<_Sp_counted_deleter>::other _My_alloc_type;struct _My_Deleter: public _My_alloc_type   {_Deleter _M_del; _My_Deleter(_Deleter __d, const _Alloc& __a): _My_alloc_type(__a), _M_del(__d) { }};protected:_My_Deleter      _M_del;  // .... ........... .... ... .....};

而引用技术基础类_Sp_counted_base又继承了 _Mutex_base进行互斥访问管理:

 template<_Lock_policy _Lp>class _Mutex_base{protected:enum { _S_need_barriers = 0 };};template<>class _Mutex_base<_S_mutex>: public __gnu_cxx::__mutex{protected:enum { _S_need_barriers = 1 };};template<_Lock_policy _Lp = __default_lock_policy>class _Sp_counted_base: public _Mutex_base<_Lp>{_Atomic_word  _M_use_count; _Atomic_word  _M_weak_count;  };

从这个数据结构看,STL库中的shared_ptr保存了被管理对象的指针的2个副本。所以使用时要注意保护好一致性。而shared_ptr内置的mutex管理,表明,shared_ptr设计的目标确实是想支持多线程应用程序。

那么这个设计目标到底实现了没有?遗憾的是没有。问题出在swap上:

  template<typename _Tp>class __shared_ptr {voidswap(__shared_ptr<_Tp, _Lp>&& __other) {std::swap(_M_ptr, __other._M_ptr);_M_refcount._M_swap(__other._M_refcount);}_Tp*         	   _M_ptr;         __shared_count<_Lp>  _M_refcount;   };

这个代码看是简单,却留下了隐患。因为OS在运行过程中,可以在一个进程或线程的任意位置产生一个调度断点,然后又去调用别的进程或线程的来跑。上面的代码如果在第一条
std::swap(_M_ptr, __other._M_ptr);
执行完立即产生一个调度断点,那么别的线程可以在这个点上调度运行,如果凑巧也执行了一个 __shared_ptr ::swap(),并且__shared_ptr this指针也是那个__shared_ptr,那么它的执行导致_M_refcount被换走。
当前一个线程再次恢复执行时,
_M_refcount._M_swap(__other._M_refcount);
实际做的是另一个other和other的交换。导致的结果是,__shared_ptr中保存的_M_ptr副本和引用技术ptr扩展类的副本内容不一致。

因为无法控制OS不在那个位置产生调度断点,保护这个调度断点的办法是,断点产生时,对执行有副作用的其他线程肯定不运行。也就是说执行这部分语句需要先获得互斥锁。因为只有一个线程能获得互斥锁,所以其他线程肯定不运行。这样就保护了这个断点。

那么退一步来说,如果__shared_ptr不保存_M_ptr,而是设法克服困难直接从引用计数中保存的那个_M_ptr真本去做指针运算符重载,那么这里只剩地2个语句了,这样是不是不用互斥锁就能解决问题呢?即使只有一个语句,由于swap这个操作,交换过程中会产生tmp副本,这样仍然不安全。

所以就是要加锁。加锁的办法,如果管理的是少量的大粒度的对象,可以使用单一的全局锁,如果管理的是大量的细粒度的对象,就要使用局部锁。对象粒度的锁。此外细粒度的锁还要小心的管理上锁顺序,防止出现死锁。

        void swap(shared_ptr<T> &ptr){Lock2 lock(*this, ptr);shared_ptr<T>::swap(ptr);}

上锁在锁对象的构造函数执行lock,析构函数中执行unlock,这是为了防止上锁区域的语句抛出异常,如果不是析构函数,unlock就可能执行不到。

注意shared_ptr构造函数中也用到了swap,其中作为局部变量的临时的shared_ptr,引用计数肯定是1,或者根本就没有引用计数,可以不上锁。

最后就是修改shared_ptr的源代码来解决这个问题。如果只是测试想法,也可以不立即修改shared_ptr的代码,而是遇到swap时,通过强制类型转换执行上锁的代码。

#include <cstdio>
#include <memory>
using namespace std;struct A {static int next;int a;A() { a=next++; printf("create A:%d\n", a);}~A() { printf("destroy A:%d\n", a);}
};
int A::next=1000;template <class T, __gnu_cxx::_Lock_policy LP=__default_lock_policy>
struct swaplock :shared_ptr<T> {typedef _Sp_counted_base<LP> counted_base;
public:struct B {T *pa;struct C: counted_base {T *_M_ptr;} *p;};struct Lock2 {bool lock1;bool lock2;__gnu_cxx::__mutex *m1;__gnu_cxx::__mutex *m2;Lock2(shared_ptr<T> &p1, shared_ptr<T> &p2){B *p= (B*)&p1;B *q= (B*)&p2;lock1=lock2=false;if (p==q) return;if (p>q) std::swap(p, q);m1= p->p;m2= q->p;if(m1 && p->p->_M_get_use_count()>1) {m1->lock();lock1 = true;}if(m2 && q->p->_M_get_use_count()>1) {m2->lock();lock2 = true;}}~Lock2(){if(lock2) m2->unlock();if(lock1) m1->unlock();}};void swap(shared_ptr<T> &ptr){Lock2 lock(*this, ptr);shared_ptr<T>::swap(ptr);}void reset() { *(shared_ptr<T>*)this = shared_ptr<T>(); }bool corrupt() {B *q= (B*)this; return q->pa!=q->p->_M_ptr;}
};int main()
{shared_ptr<A>  p(new A);shared_ptr<A>  p2(new A);printf("1:%d, 2:%d\n", p->a, p2->a);((swaplock<A>&)p).swap(p2);printf("1:%d, 2:%d\n", p->a, p2->a);if( ((swaplock<A>&)p).corrupt()) {printf("error1\n");}if( ((swaplock<A>&)p2).corrupt()) {printf("error2\n");}p=p2;printf("1:%d, 2:%d\n", p->a, p2->a);return 0;
}

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

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

相关文章

使用表单系统快速搭建邀请和签到系统

在组织活动时&#xff0c;邀请和签到环节往往是活动成败的关键之一。传统的纸质邀请和签到方式不仅费时费力&#xff0c;还容易出现各种问题&#xff0c;例如名单遗漏、签到混乱等。而使用TDuckX“搭建邀请和签到系统”将彻底改变这一现状&#xff0c;为活动组织者提供了一种高…

python单元测试入门

编写基本的单元测试来验证代码的行为。 使用的库&#xff1a;unittest 单元测试框架 python的unittest库的基本单元测试框架可以表示为&#xff1a; import unittestclass XXXTests(unittest.TestCase): # 第一个测试集classmethoddef setUpClass(self):...self.x, self.y …

STM32蓝牙HID实战:打造低功耗、高性能的客制化键盘

一、项目概述 本项目旨在使用STM32单片机打造一款功能强大的蓝牙客制化键盘&#xff0c;它拥有以下特点&#xff1a; 九键布局&#xff0c;小巧便携: 满足日常使用需求&#xff0c;方便携带。全键可编程: 所有按键和旋钮均可通过电脑软件自定义快捷键&#xff0c;实现个性化功…

curl代理用户名或密码出现特殊字符时需要转义

举例&#xff1a;使用代理127.0.0.1:3128访问百度, 用户名peter, 密码123! 密码中包含&#xff0c;需要转义。 查询在线URL编码工具, %21是!的URL编码&#xff0c;curl使用方法如下&#xff1a; curl -x peter:123%21127.0.0.1:3128 https://www.baidu.com参考 https://www.u…

locally Holder continuous (non-Lipschitz)

locally Holder continuous (non-Lipschitz) Holder连续性和Lipschitz连续性是描述函数局部或全局性质的两种方式&#xff0c;它们之间存在联系但并不等同。 如果一个函数(f)在某区间上满足Lipschitz条件&#xff0c;即存在常数(K > 0)&#xff0c;使得对任意(x, y)在该区间…

如何用java语言+若依开源框架开发一套数字化产科系统 数字化产科管理平台源码

如何用java语言若依开源框架开发一套数字化产科系统 数字化产科管理平台源码 要使用Java语言和若依&#xff08;RuoYi&#xff09;开源框架来开发一个数字化产科系统&#xff0c;你需要遵循一系列步骤&#xff0c;从环境搭建到系统设计与开发&#xff0c;再到测试与部署。 以下…

2023年问界M9 EV 问界M9增程维修手册和电路图线路图资料更新

此次更新了2023年问界M9 EV及问界M9增程维修手册和电路图资料&#xff0c;覆盖市面上99%车型&#xff0c;包括维修手册、电路图、新车特征、车身钣金维修数据、全车拆装、扭力、发动机大修、发动机正时、保养、电路图、针脚定义、模块传感器、保险丝盒图解对照表位置等等&#…

Redis的八种数据类型介绍

Redis 是一个高性能的键值存储&#xff0c;它支持多种丰富的数据类型。每种数据类型都有其特定的用途和底层实现。下面我将介绍 Redis 支持的主要数据类型及其背后的数据结构。 本人这里还有几篇详细的Redis用法文章&#xff0c;可以用来进阶康康&#xff01; 1. 字符串 (Stri…

macOS笔记

1、MAC中抹掉就是格式化&#xff1b; 2、MAC中拔出U盘&#xff1a;在桌面找到U盘&#xff0c;点击右键显示“推出***”&#xff0c;点击退出。 3、MAC系统版本: macOS 11: Big Sur macOS 12 Monterey macOS 13 Ventura macOS 14 Sonoma macOS 15 Sequoia 4、通用快捷键&#xf…

关于Spring容器的一些理解:如何将类交给Spring容器管理,Spring容器如何实现将类进行自动注册

如果我要将一个类丢给Spring容器管理&#xff0c;我需要怎么做&#xff1f; 如果你想将一个类交给Spring容器管理&#xff0c;使其成为Spring的一个bean&#xff0c;通常可以通过以下几种方式来实现&#xff1a; 方式一、使用注解方式&#xff1a; Component 及其衍生注解&a…

ubuntu24.04LTS防火墙设置

Ubuntu24.04LTS开箱自带ufw&#xff0c;一定程度避免了开机下载ufw被攻击&#xff0c;excellent 转载aliyun教程 sudo ufw enbale可以启用并且开机自启(显示有效&#xff0c;未nmap实测) 教程3 转载自CSDN 完整格式如下&#xff1a; # 禁止IP连接端口 sudo ufw deny proto tc…

Cherno 游戏引擎笔记 (45~60)

有几个部分的笔记以图片形式呈现&#xff08;如果没找到文本可以查看是否遗漏了图片笔记&#xff09; My Github REPO(GitHub - JJJJJJJustin/Nut: The game_engine which learned from Cherno) 源码笔记&#xff0c;希望帮到你 :-} ---Shader Library&#xff08;着色器库&…

南京观海微电子----AC/DC、DC/DC转换器知识

什么是AC&#xff1f; Alternating Current&#xff08;交流&#xff09;的首字母缩写。 AC是大小和极性&#xff08;方向&#xff09;随时间呈周期性变化的电流。 电流极性在1秒内的变化次数被称为频率&#xff0c;以Hz为单位表示。 什么是DC? Direct Current&#xff08;直流…

visual studio远程调试

场景一&#xff08;被远程调试的电脑&#xff09; 确定系统位数 我这里是x64的 找到msvsmon.exe msvsmon.exe目录位置解释&#xff1a; “F:\App\VisualStudio\an\Common7\IDE\”是visual studio所在位置、 “Remote Debugger\”是固定位置、 “x64”是系统位数。 拼起来就是…

grid布局下的展开/收缩过渡效果【vue/已验证可正常运行】

代码来自GPT4o&#xff1a;国内官方直连GPT4o <template><div class"container"><button class"butns" click"toggleShowMore">{{ showAll ? 收回 : 显示更多 }}</button><transition-group name"slide-fade&…

数据库原理实验报告第二次-SQL Server SSMS工具创建和管理数据库及数据表.

题目 1、使用SSMS工具创建名为ecommerce的数据库&#xff0c;并查看或修改数据库属性 2、在数据库ecommerce中创建如下表&#xff1a; &#xff08;1&#xff09;商品类别表category 字段名 数据类型 允许NULL值 约束 字段说明 catno int 否 主键 商品类别编号 ca…

AI是在帮助开发者还是取代他们?

一&#xff1a;介绍 生成式人工智能&#xff08;AIGC&#xff09;在软件开发领域的应用确实为开发者带来了很多便利和效率提升。AI工具可以通过代码生成、错误检测、自动化测试等功能&#xff0c;帮助开发者更快速地开发和优化软件&#xff0c;减少重复性工作&#xff0c;提高…

哈喽GPT-4o,对GPT-4o 论文速写的思考与探索

作为一款强大的语言模型&#xff0c;ChatGPT 在论文写作上具备显著优势。它能够辅助学者或研究人员自动创建论文框架、摘要、文献综述及论文段落&#xff08;如引言、方法、结果、结论等&#xff09;。此外&#xff0c;ChatGPT 还能优化论文结构、润色、降低内容重复率&#xf…

比Proxmox VE更易用的免费虚拟化平台

之前虚拟化一直玩Proxmox VE&#xff0c;最近发现一个更易用的虚拟化软件CSYun&#xff0c;他与Proxmox VE类似&#xff0c;都是一个服务器虚拟化平台。它不像VMware ESXi那么复杂&#xff0c;对于个人使用者和中小企业是一个比较好的选择。 这个软件所在的网址为&#xff1a;…

【Python】已解决TypeError: init() got an unexpected keyword argument ‘threshold’

文章目录 一、分析问题背景二、可能出错的原因三、错误代码示例四、正确代码示例五、注意事项 已解决TypeError: init() got an unexpected keyword argument ‘threshold’ 一、分析问题背景 在Python编程中&#xff0c;遇到“TypeError: init() got an unexpected keyword …