shared_ptr 引用计数相关问题

news2024/11/17 16:53:47

前言

智能指针是 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.coloradmin.cn/o/1664529.html

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

相关文章

从零自制docker-12-【overlayfs】

文章目录 overlayfsexec.Command("tar", "-xvf", busyboxTarURL, "-C", busyboxURL).CombinedOutput()exec.Command格式差异 挂载mount卸载unmount代码地址结果演示 overlayfs 就是联合文件系统&#xff0c;将多个文件联合在一起成为一个统一的…

【VTKExamples::Rendering】第五期 环形阵列Rotations

很高兴在雪易的CSDN遇见你 VTK技术爱好者 QQ:870202403 公众号:VTK忠粉 前言 本文分享VTK样例环形阵列Rotations,希望对各位小伙伴有所帮助! 感谢各位小伙伴的点赞+关注,小易会继续努力分享,一起进步! 你的点赞就是我的动力(^U^)ノ~YO 1. Rotations

程序环境和预处理、编译链接过程、编译的几个阶段、运行环境、预定义符号等的介绍

文章目录 前言一、程序的翻译环境和执行环境二、编译链接过程三、编译的几个阶段四、运行环境五、预定义符号总结 前言 程序环境和预处理、编译链接过程、编译的几个阶段、运行环境、预定义符号的介绍。 一、程序的翻译环境和执行环境 在 ANSI C 的任何一种实现中&#xff0c…

DDR5和LPDDR4/5 命令解析

关键名称介绍 DDR5 SDRAM和LPDDR4/5都采用了高级的命令集来支持更高效的内存管理和操作,其中“Multi-purpose command (MPC)”、“Mode Register Read (MRR)”、“Mode Register Write (MRW)”,以及“Write Pattern Command”是几种关键的命令类型,它们在内存初始化、配置和…

力扣 5-11

704. 二分查找 给定一个 n 个元素有序的&#xff08;升序&#xff09;整型数组 nums 和一个目标值 target &#xff0c;写一个函数搜索 nums 中的 target&#xff0c;如果目标值存在返回下标&#xff0c;否则返回 -1。 这道题目的前提是数组为有序数组&#xff0c;同时题目还强…

028.实现 strStr()

题意 给你两个字符串 haystack 和 needle &#xff0c;请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标&#xff08;下标从 0 开始&#xff09;。如果 needle 不是 haystack 的一部分&#xff0c;则返回 -1 。 难度 简单 示例 例 1 输入&#xff1a;hays…

Java----数组的定义和使用

1.数组的定义 在Java中&#xff0c;数组是一种相同数据类型的集合。数组在内存中是一段连续的空间。 2.数组的创建和初始化 2.1数组的创建 在Java中&#xff0c;数组创建的形式与C语言又所不同。 Java中数组创建的形式 T[] 数组名 new T[N]; 1.T表示数组存放的数据类型…

1290.二进制链表转整数

给你一个单链表的引用结点 head。链表中每个结点的值不是 0 就是 1。已知此链表是一个整数数字的二进制表示形式。 请你返回该链表所表示数字的 十进制值 。 示例 1&#xff1a; 输入&#xff1a;head [1,0,1] 输出&#xff1a;5 解释&#xff1a;二进制数 (101) 转化为十进制…

静态住宅代理 IP 的影响

在不断发展的在线业务和数字营销领域&#xff0c;保持领先地位势在必行。在业界掀起波澜的最新创新之一是静态住宅代理 IP 的利用。这些知识产权曾经是为精通技术的个人保留的利基工具&#xff0c;现在正在成为各行业企业的游戏规则改变者。 一、静态住宅代理IP到底是什么&…

LeetCode/NowCoder-链表经典算法OJ练习1

目录 说在前面 题目一&#xff1a;移除链表元素 题目二&#xff1a;反转链表 题目三&#xff1a;合并两个有序链表 题目四&#xff1a;链表的中间节点 SUMUP结尾 说在前面 dear朋友们大家好&#xff01;&#x1f496;&#x1f496;&#x1f496;数据结构的学习离不开刷题…

实现树莓派DS18B20读取温度(OneWire)

简介 使用的是树莓派3B, Go编程实现OneWire方式读取DS18B20温度。 接线 DS18B20 包含经典三线&#xff0c; VCC和GND自不必说&#xff0c; 主要的是DQ线&#xff0c; 需要接4.7K的上拉电阻&#xff0c; 即4.7K欧姆的电阻接到DQ和VCC&#xff0c; 否则树莓派识别不到DS18B20&am…

2024kali linux上安装java8

1 kali下载Java 8安装包 访问Oracle官网或其他可信的Java下载站点&#xff0c;如华为云的开源镜像站&#xff08;例如&#xff1a;https://repo.huaweicloud.com/java/jdk/8u202-b08/jdk-8u202-linux-x64.tar.gz&#xff09;。 确保下载的是与你的Kali Linux系统架构&#xf…

Covalent Network(CQT)通过 “新曙光” 计划实现重要里程碑,增强以太坊时光机,提供 30% 的年化质押收益率

Covalent Network&#xff08;CQT&#xff09;作为集成超过 280 条区块链&#xff0c;并服务于超过 2.8 亿个钱包的领先结构化数据基础设施层&#xff0c;宣布了其战略计划 “新曙光” 中的一个重要进展。随着网络升级并完成了准备工作的 75%&#xff0c;这将为即将部署的以太坊…

2024数维杯数学建模B题完整论文讲解(含每一问python代码+结果+可视化图)

大家好呀&#xff0c;从发布赛题一直到现在&#xff0c;总算完成了2024数维杯数学建模挑战赛生物质和煤共热解问题的研究完整的成品论文。 本论文可以保证原创&#xff0c;保证高质量。绝不是随便引用一大堆模型和代码复制粘贴进来完全没有应用糊弄人的垃圾半成品论文。 B题论…

基于单片机的直流电机检测与控制系统

摘要&#xff1a; 文章设计一款流电机控制系统&#xff0c;以 STC89C51 作为直流电机控制系统的主控制器&#xff0c;采用 LM293 做为驱动器实现 对直流电机的驱动&#xff0c;采用霍尔实现对直流电机速度的检测&#xff1b;本文对直流电机控制系统功能分析&#xff0c;选择确…

探索Linux:深入理解各种指令与用法

文章目录 cp指令mv指令cat指令more指令less指令head指令tail指令与时间相关的指令date指令 cal指令find指令grep指令zip/unzip指令总结 上一个Linux文章我们介绍了大部分指令&#xff0c;这节我们将继续介绍Linux的指令和用法。 cp指令 功能&#xff1a;复制文件或者目录 语法…

TMS320F280049 CLB模块--FSM(3)

功能框图 FSM有效状态机内部框图如下图所示&#xff0c;可以看到内部有S0 / S1两个状态和下一状态的跳转查找表。还有个输出查找表。 下图是FSM LUT的示意框图。FSM还可以工作在3输入或4输入的查找表模式下。对于输入&#xff0c;EXTRA_EXT_IN1/0可以替换S0/1。 寄存器 参考文…

词令蚂蚁庄园今日答案如何在微信小程序查看蚂蚁庄园今天问题的正确答案?

词令蚂蚁庄园今日答案如何在微信小程序查看蚂蚁庄园今天问题的正确答案&#xff1f; 1、打开微信&#xff0c;点击搜索框&#xff1b; 2、打开搜索页面&#xff0c;选择小程序搜索&#xff1b; 3、在搜索框&#xff0c;输入词令搜索点击进入词令微信小程序&#xff1b; 4、打开…

vivado Kintex-7 配置存储器器件

Kintex-7 配置存储器器件 下表所示闪存器件支持通过 Vivado 软件对 Kintex -7 器件执行擦除、空白检查、编程和验证等配置操作。 本附录中的表格所列赛灵思系列非易失性存储器将不断保持更新 &#xff0c; 并支持通过 Vivado 软件对其中所列非易失性存储器 进行擦除、…

在Qt工具栏上实现矩阵并排的按钮效果源码

如果这个要用MFC去实现头皮都得掉一层&#xff0c;建议大家以后要写GUI方面的小工具尽量转QT或其他吧&#xff0c;MFC真不适合搞这种花里胡哨的界面. 在Qt工具栏上实现矩阵并排的按钮效果源码如下&#xff1a; #include "mainwindow.h" #include "ui_mainwind…