shared_ptr 引用计数相关问题

前言

智能指针是 C++11 增加的非常重要的特性,并且也是面试的高频考点,本文主要解释以下几个问题:

  • 引用计数是怎么共享的、怎么解决并发问题的
  • 资源释放时,控制块的内存释放吗
  • weak_ptr 怎么判断对象是否已经释放

文中源码用的是 LLVM libcxx-3.5.0,为了方便理解有部分修改,关于自定义删除器和内存池的部分都删掉了。

可以想象 std::shared_ptr 对象在内存中是这样(weak_ptr 内存布局与 shard_ptr 类似):

shared_ptr 控制块

shared_ptr

部分源码如下所示:

template <class _Tp>
class shared_ptr {public:using element_type = _Tp;private:element_type*        __ptr_;__shared_weak_count* __cntrl_;public:template<class _Yp>shared_ptr(_Yp* __p): __ptr_(__p) {__cntrl_ = new __shared_weak_count();}shared_ptr(const shared_ptr& __r) : __ptr_(__r.__ptr_),__cntrl_(__r.__cntrl_) {if (__cntrl_ != nullptr) {__cntrl_->__add_shared();}}~shared_ptr() {if (__cntrl_ != nullptr) {__cntrl_->__release_shared();}}
};

成员变量

element_type*        __ptr_;
__shared_weak_count* __cntrl_;

可以看到有两个成员变量,一个是指向资源的指针,另一个是指向控制块的指针。

class __shared_count {protected:long __shared_owners_;public:__shared_count(long __refs = 0) : __shared_owners_(__refs) {}
};class __shared_weak_count : private __shared_count {long __shared_weak_owners_;public:__shared_weak_count(long __refs = 0): __shared_count(__refs),__shared_weak_owners_(__refs) {}
};

__shared_weak_count 又继承了 __shared_count,它们各有一个成员变量分别记录 shared_ptr 和 weak_ptr 的数量。

需要注意的是计数的初始值是 0,不是 1。

复制构造函数

shared_ptr(const shared_ptr& __r) : __ptr_(__r.__ptr_),__cntrl_(__r.__cntrl_) {if (__cntrl_ != nullptr) {__cntrl_->__add_shared();}
}

复制的时候将指针指向同一个资源和同一个控制块,然后增加控制块的计数值,这样就能共享计数值了。那它是怎么保证并发安全的呢?

template <class T>
T increment(T& t) {return __sync_add_and_fetch(&t, 1);
}void __shared_count::__add_shared() {increment(__shared_owners_);
}void __shared_weak_count::__add_shared() {__shared_count::__add_shared();
}

它的实现非常简单,就是调用了原子函数将 __shared_owners_ 的值加 1。

可以看到它是通过使用原子函数来解决并发问题的,这样比使用锁的并发性要好一些。

析构函数

~shared_ptr() {if (__cntrl_ != nullptr) {__cntrl_->__release_shared();}
}

析构函数直接调用了一个 __release_shared 函数,它的代码如下:

template <class T>
T decrement(T& t)  {return __sync_add_and_fetch(&t, -1);
}bool __shared_count::__release_shared() {if (decrement(__shared_owners_) == -1) {__on_zero_shared();return true;}return false;
}void __shared_weak_count::__release_shared() {if (__shared_count::__release_shared()) {__release_weak();}
}void __shared_weak_count::__release_weak() {if (decrement(__shared_weak_owners_) == -1) {__on_zero_shared_weak();}
}

在 __shared_weak_count::__release_shared() 内首先调用 __shared_count::__release_shared() 将 __shared_owners_ 的值减 1,如果没有其他 shared_ptr 指向该资源了,就释放资源占用的内存。然后又调用 __shared_weak_count::__release_weak() 将 __shared_weak_owners_ 的值减 1,如果也没有其他 weak_ptr 指向该资源了,就释放控制块占用的资源。

可以看出当最后一个 shared_ptr 析构时,若没有其他 weak_ptr 指向该资源控制块的内存会被释放;否则不会立即释放。那控制块什么时候释放呢?请看下文。

weak_ptr

部分源码如下所示:

template<class _Tp>
class weak_ptr {public:using element_type = _Tp;private:element_type*        __ptr_;__shared_weak_count* __cntrl_;
public:weak_ptr(shared_ptr<_Yp> const& __r): __ptr_(__r.__ptr_),__cntrl_(__r.__cntrl_) {if (__cntrl_) {__cntrl_->__add_weak();}}~weak_ptr() {if (__cntrl_) {__cntrl_->__release_weak();}}shared_ptr<_Tp> lock() const {shared_ptr<_Tp> __r;__r.__cntrl_ = __cntrl_ ? __cntrl_->lock() : __cntrl_;if (__r.__cntrl_) {__r.__ptr_ = __ptr_;}return __r;}
};

成员变量

element_type*        __ptr_;
__shared_weak_count* __cntrl_;

成员变量与 shared_ptr 一样,也是一个指向资源的指针和一个指向控制块的指针。

构造函数

weak_ptr(shared_ptr<_Yp> const& __r): __ptr_(__r.__ptr_),__cntrl_(__r.__cntrl_) {if (__cntrl_) {__cntrl_->__add_weak();}
}

构造函数很简单,就是将指针指向 shared_ptr 所指向的资源和控制块,然后调用 __add_weak 将弱引用计数加 1。

void __shared_weak_count::__add_weak() {increment(__shared_weak_owners_);
}

析构函数

~weak_ptr() {if (__cntrl_) {__cntrl_->__release_weak();}
}

析构函数就是调用 __shared_weak_count::__release_weak(),将 __shared_weak_owners_ 的值减 1,当没有其他 shared_ptr & weak_ptr 指向该资源时释放控制块,防止内存泄露。

lock

lock 是 weak_ptr 中很重要的函数,如果我们想使用 weak_ptr 指向的资源,必须先调用 lock() 函数获取一个 shared_ptr。

shared_ptr<_Tp> lock() const {shared_ptr<_Tp> __r;__r.__cntrl_ = __cntrl_ ? __cntrl_->lock() : __cntrl_;if (__r.__cntrl_) {__r.__ptr_ = __ptr_;}return __r;
}__shared_weak_count* __shared_weak_count::lock()  {long object_owners = __shared_owners_;while (object_owners != -1) {if (__sync_bool_compare_and_swap(&__shared_owners_,object_owners,object_owners+1)) {return this;}object_owners = __shared_owners_;}return 0;
}

在 weak_ptr::lock() 中首先定义一个了 shared_ptr,然后为它设置指向控制块的指针,如果设置成功再设置指向资源的指针。

__shared_weak_count::lock() 中先获取当前 shared_ptr 的数量,只要指向的资源还存在(__shared_owners_ 不为 -1),就将计数加 1,然后返回控制块指针。

总结

引用计数是怎么共享的,怎么解决并发问题的?

通过使多个 shared_ptr 内部的 __cntrl_ 指向同一个控制块实现计数共享。

使用原子性函数来操作记录计数的变量来解决并发问题。

资源释放时,控制块的内存释放吗?

如果没有其他 weak_ptr 指向该资源,控制块的内存会释放;如果有其他 weak_ptr 指向该资源,那控制块的内存不会释放,由最后一个 weak_ptr 析构时释放。

weak_ptr 怎么判断对象是否已经释放?

使用 weak_ptr 时需要调用 lock() 函数升级成 shared_ptr,此时会检查 __shared_owners_ 看资源是否已释放。

参考资料

  • 《Effective Modern C++》
  • libcxx-3.5.0

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

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

相关文章

XMind 2023 v23.05.2660软件安装教程(附软件下载地址)

软件简介&#xff1a; 软件【下载地址】获取方式见文末。注&#xff1a;推荐使用&#xff0c;更贴合此安装方法&#xff01; XMind 2023 v23.05.2660被视为顶尖思维导图软件&#xff0c;其界面简洁清爽&#xff0c;功能布局直观简单&#xff0c;摒弃繁复不实。尽管体积小巧&a…

RabbitMQ(安装配置以及与SpringBoot整合)

文章目录 1.基本介绍2.Linux下安装配置RabbitMQ1.安装erlang环境1.将文件上传到/opt目录下2.进入/opt目录下&#xff0c;然后安装 2.安装RabbitMQ1.进入/opt目录&#xff0c;安装所需依赖2.安装MQ 3.基本配置1.启动MQ2.查看MQ状态3.安装web管理插件4.安装web管理插件超时的解决…

【R语言与统计】SEM结构方程、生物群落、多元统计分析、回归及混合效应模型、贝叶斯、极值统计学、meta分析、copula、分位数回归、文献计量学

统计模型的七大类&#xff1a;一&#xff1a;多元回归 在研究变量之间的相互影响关系模型时候&#xff0c;用到这类方法&#xff0c;具体地说&#xff1a;其可以定量地描述某一现象和某些因素之间的函数关系&#xff0c;将各变量的已知值带入回归方程可以求出因变量的估计值&…

P8803 [蓝桥杯 2022 国 B] 费用报销

P8803 [蓝桥杯 2022 国 B] 费用报销 分析 最值问题——DP 题意分析&#xff1a;从N张票据中选&#xff0c;且总价值不超过M的票据的最大价值&#xff08;背包问题&#xff09; K天限制 一、处理K天限制&#xff1a; 1.对于输入的是月 日的格式&#xff0c;很常用的方式是…

笨方法自学python(二)-注释

注释和#号 程序里的注释是很重要的。它们可以用自然语言告诉你某段代码的功能是什么。在你想要临时移除一段代码时&#xff0c;你还可以用注解的方式将这段代码临时禁用。 # A comment, this is so you can read your program later. # Anything after the # is ignored by py…

SpringBoot自定义初始化sql文件 支持多类型数据库

我在resources目录下有init.sql初始化sql语句 指定sql文件的地址 sql内容如下&#xff1a; /*角色表*/ INSERT INTO #{schema}ccc_base_role (id, create_time, create_user_id, is_delete, role_name, status, update_time, update_user_id) VALUES(b89e30d81acb88448d412…

phpmyadmin配置文件权限错误

错误信息 配置文件权限错误,不应任何用户都能修改! 解决办法 找到phpmyadmin所在目录 给phpmyadmin目录授权755 chmod -R 755 phpmyadmin验证服务是否可以正常访问

履带车配置 一些小细节

履带车配置 一些小细节 485/CAN模式 自动识别上电后第一帧是485还是CAN指令&#xff0c;就进入对应通讯模式用485通讯这个驱动器通讯可以了&#xff0c;你再接CAN通讯&#xff0c;这个驱动器必须断电重启一下&#xff1b;不能在通电模式下切换 驱动器拨码开关 两个驱动器的…

PCIE协议-1

1. PCIe结构拓扑 一个结构由点对点的链路组成&#xff0c;这些链路将一组组件互相连接 - 图1-2展示了一个结构拓扑示例。该图展示了一个称为层级结构的单一结构实例&#xff0c;由一个根复合体&#xff08;Root Complex, RC&#xff09;、多个端点&#xff08;I/O设备&#xf…

增加表空间的数据文件

Oracle从入门到总裁:​​​​​​https://blog.csdn.net/weixin_67859959/article/details/135209645 表空间创建完成后&#xff0c;后期还可以为表空间增加数据文件&#xff0c;以扩大数据的存储空间。增加表空间数据文件的基本语法结构如下所示。 ALTER TABLESPACE 表空间名…

【免费】在线识别通用验证码接口

模块优势价格5元1000次&#xff0c;每天免费100次api文档支持 使用量小的完全够用了 <?phpfunction Post_base64($base64_str){$url http://api.95man.com:8888/api/Http/Recog?Taken41******QK&imgtype1&len0 ; $fields array( ImgBase64>$base64_str); $ch…

day3_prefixSum

一、前缀和技巧 重点 前缀和技巧适用于快速、频繁地计算一个索引区间内的元素之和 个人理解&#xff1b;预计算&#xff0c;空间换时间 1.(一维数组的前缀和)303区域和检索-数组不可变 获取闭区间值 [left,right] -> preSum[right 1] - preSum[left],其中preSum[right…