【问题标题】:How does weak_ptr work?weak_ptr 是如何工作的?
【发布时间】:2011-08-05 23:42:17
【问题描述】:

我了解如何使用weak_ptrshared_ptr。通过计算其对象中的引用数,我了解shared_ptr 的工作原理。 weak_ptr 是如何工作的?我尝试通读 boost 源代码,但我对 boost 不够熟悉,无法理解它使用的所有东西。

谢谢。

【问题讨论】:

标签: c++ boost weak-references tr1 weak-ptr


【解决方案1】:

shared_ptr 使用额外的“计数器”对象(又名“共享计数”或“控制块”)来存储引用计数。 (顺便说一句:那个“计数器”对象也存储了删除器。)

每个shared_ptrweak_ptr 都包含一个指向实际指针对象的指针,以及一个指向“计数器”对象的第二个指针。

为了实现weak_ptr,“计数器”对象存储了两个不同的计数器:

  • “使用计数”是指向对象的shared_ptr 实例数。
  • “弱计数”是指向对象的weak_ptr 实例的数量,如果“使用计数”仍然 > 0,则加一。

当“使用计数”达到零时,指针被删除。

当“弱计数”达到零时,“计数器”辅助对象被删除(这意味着“使用计数”也必须为零,见上文)。

当您尝试从 weak_ptr 获取 shared_ptr 时,库会自动检查“使用计数”,如果它 > 0,则将其递增。如果成功,您将获得shared_ptr。如果“使用计数”已经为零,您将获得一个空的 shared_ptr 实例。


编辑:现在,为什么当两个计数都降为零时,他们将弱计数加一而不是释放“计数器”对象?好问题。

当“使用计数”和“弱计数”都降为零时,替代方法是删除“计数器”对象。这是第一个原因:并非在每个平台上都可以原子地检查两个(指针大小的)计数器,即使在哪里,也比只检查一个计数器更复杂。

另一个原因是删除器必须保持有效,直到它完成执行。由于删除器存储在“计数器”对象中,这意味着“计数器”对象必须保持有效。考虑如果某个对象有一个shared_ptr 和一个weak_ptr,并且它们在并发线程中同时被重置,会发生什么情况。假设shared_ptr 排在第一位。它将“使用计数”减少到零,并开始执行删除程序。现在weak_ptr 将“弱计数”减少到零,并发现“使用计数”也为零。因此,它删除了“计数器”对象,以及删除器。当删除器仍在运行时。

当然会有不同的方法来确保“计数器”对象保持活动状态,但我认为将“弱计数”增加一个是一种非常优雅和直观的解决方案。 “弱计数”成为“计数器”对象的引用计数。由于shared_ptrs 也引用了计数器对象,他们也必须增加“弱计数”。

一个可能更直观的解决方案是增加每个shared_ptr 的“弱计数”,因为每个shared_ptr 持有都是对“计数器”对象的引用。

为所有shared_ptr 实例添加一个只是一种优化(在复制/分配shared_ptr 实例时保存一个原子增量/减量)。

【讨论】:

  • 他们不检查弱计数和使用计数是否为零,然后删除计数器对象。但相反,如果使用计数不为零,它们会将弱计数增加一,然后只检查弱计数。这是为什么呢?
  • 由于这只是一个评论有点难以解释,我更新了我的答案。
  • 也许他们只为每个 shared_ptr 添加 1 而不是 1 的原因是,当他们需要触摸计数器对象来调用删除器时,他们只需要触摸一次计数器对象,而不是每次他们减少使用计数器?
  • 他们必须触摸“计数器”对象才能更新“使用计数”。但是,更新“弱计数”也需要双宽度 CAS(在许多平台上不可用)或第二条 CAS 指令(非常昂贵)。 “one for all”解决方案只需要每个指针一个额外的 CAS,即在指针被删除之后。 (初始化不需要原子指令,因为当时没有其他线程访问“计数器”对象)。
【解决方案2】:

基本上,“weak_ptr”是一个普通的“T*”指针,可以让您在后面的代码中恢复强引用,即“shared_ptr”。

就像一个普通的 T*,weak_ptr 不做任何引用计数。在内部,为了支持对任意类型 T 的引用计数,STL(或任何其他实现这种逻辑的库)创建了一个包装对象,我们称之为“Anchor”。 “Anchor”的存在只是为了实现我们需要的引用计数和“当计数为零时,调用删除”行为。

在强引用中,shared_ptr 实现其副本、operator=、构造函数、析构函数和其他相关 API 以更新“锚”的引用计数。这就是 shared_ptr 确保您的“T”在有人使用它时一直存在的方式。在“weak_ptr”中,这些相同的 API 只是复制实际的 Anchor ptr。它们不会更新引用计数。

这就是为什么“weak_ptr”最重要的 API 都“过期”而名不副实的“锁”的原因。 “已过期”告诉您底层对象是否仍然存在 - 即“它是否已经删除了自己,因为所有强引用都超出了范围?”。 “Lock”将(如果可以)将weak_ptr 转换为强引用shared_ptr,恢复引用计数。

顺便说一句,“锁”对于该 API 来说是一个糟糕的名称。您不是(只是)调用互斥锁,而是从弱引用创建强引用,并具有“锚”作用。两个模板的最大缺陷是它们没有实现 operator->,所以要对你的对象做任何事情,你必须恢复原始的“T*”。他们这样做主要是为了支持“shared_ptr”之类的东西,因为原始类型不支持“->”运算符。

【讨论】:

  • 您写道:在“weak_ptr”中,那些相同的 API 只是复制实际的 Anchor ptr。它们不会更新引用计数。 这是不正确的;他们更新 weak 引用计数,它管理 Anchor 本身的生命周期。否则,你怎么知道何时释放 Anchor 自己的内存?另外,std::shared_ptr确实实现了operator->;我不知道你是怎么想到它没有的。
  • STL是Standard Template Library的缩写,即容器、迭代器等。智能指针不在其中。
猜你喜欢
  • 2021-07-28
  • 1970-01-01
  • 2012-11-21
  • 2017-05-20
  • 2012-10-04
  • 2018-01-12
  • 2022-01-16
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多