weak_ptr是如何工作的?

77

我知道如何使用weak_ptrshared_ptr。我知道shared_ptr是通过计算其对象中引用的数量来工作的。那么weak_ptr是如何工作的呢?我尝试阅读了boost源代码,但我对boost不够熟悉,无法理解它使用的所有内容。

谢谢。


6
另请参见:共享指针是如何工作的? - James McNellis
1
请参阅shared_ptr实现注意事项 - ks1322
2个回答

139

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

每个 shared_ptrweak_ptr 包含指向实际资源的指针和指向“计数器”对象的第二个指针。

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

  • "使用计数"是指向该对象的 shared_ptr 实例的数量。
  • "弱引用计数"是指向该对象的weak_ptr 实例的数量,如果 "使用计数" 仍然 > 0,则再加1。

当“使用计数”达到零时,将删除点ee。

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

当您尝试从 weak_ptr 获得一个 shared_ptr 时,库会原子地检查“使用计数”,如果它 > 0,则增加它。如果成功,您将获得您的 shared_ptr。如果“使用计数”已经为零,则会返回一个空的shared_ptr 实例。


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

另一种方法是,在“使用计数”和“弱引用计数”都降到零时删除“计数器”对象。以下是第一个原因:在每个平台上都不可能原子地检查两个(指针大小的)计数器,即使在可以实现的情况下,也比仅检查一个计数器要复杂。

另一个原因是,在删除器完成执行之前,必须保证其有效。由于删除器存储在"counter"对象中,这意味着"counter"对象必须保持有效。考虑一下如果有一个shared_ptr和一个weak_ptr指向某个对象,并且它们在并发线程中同时被重置会发生什么。假设shared_ptr先出现。它将"use count"减少到零,并开始执行删除器。现在weak_ptr将"weak count"减少到零,并发现"use count"也为零。因此,它删除了"counter"对象,以及其中的删除器。而删除器仍在运行。
当然,有不同的方法来确保"counter"对象保持活动状态,但我认为增加"weak count"是一种非常优雅和直观的解决方案。"weak count"成为"counter"对象的引用计数。由于shared_ptr也引用计数器对象,它们也必须增加"weak count"。
可能更直观的解决方案是,对于每个单独的shared_ptr都增加"weak count",因为每个单独的shared_ptr都持有对"counter"对象的引用。
对于所有shared_ptr实例添加一个只是一种优化(在复制/分配shared_ptr实例时,可以节省一个原子递增/递减)。

1
他们没有检查弱引用计数和使用计数是否为零,然后再删除计数器对象。相反,如果使用计数非零,他们会将弱引用计数增加一,然后只检查弱引用计数。为什么这样做? - Johannes Schaub - litb
1
由于仅通过注释解释有些困难,我更新了我的答案。 - Paul Groke
也许他们之所以只增加1而不是为每个shared_ptr都增加1的原因是,这样他们只需要在需要调用删除器时一次性触碰计数器对象,而不是每次减少使用计数器时都要触碰它? - Johannes Schaub - litb
3
为了更新“使用计数”,他们必须无论如何触碰“计数器”对象。然而,更新“弱引用计数”也需要双宽CAS(这在许多平台上不可用)或第二个CAS指令(代价相当高)。 “一锅端”解决方案只需要每个指针额外的一个CAS操作,在指针被删除后进行。(初始化不需要原子操作,因为此时没有其他线程访问“计数器”对象)。 - Paul Groke

-10

基本上,"weak_ptr" 是一个普通的 "T*" 指针,它允许你在代码中稍后恢复一个强引用,即 "shared_ptr"。

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

在强引用中,shared_ptr 实现了其拷贝、赋值运算符、构造函数、析构函数和其他相关 API 来更新 "Anchor" 的引用计数。这就是 shared_ptr 如何确保你的 "T" 存活的时间恰好与有人使用它的时间一样长。而在 "weak_ptr" 中,这些相同的 API 只是简单地复制实际的 Anchor 指针。它们不会更新引用计数。

这就是为什么 "weak_ptr" 最重要的 API 是 "expired" 和命名不佳的 "lock"。"Expired" 告诉你底层对象是否仍然存在-即 "它是否已经因为所有强引用超出作用域而被删除?"。"Lock"(如果可能的话)将 weak_ptr 转换为强引用 shared_ptr,恢复引用计数。

顺便说一句,“lock”是那个API的一个糟糕的名称。你不仅仅是调用互斥锁,而且还通过“Anchor”创建了一个弱引用到强引用的转换。两个模板中最大的缺陷是它们没有实现operator->,所以要对对象进行任何操作,你必须恢复原始的“T*”。他们这样做主要是为了支持像“shared_ptr”这样的东西,因为基本类型不支持“->”运算符。


5
您写道:“在‘weak_ptr’中,这些API只是复制实际的Anchor指针。它们不会更新引用计数。” 这是不正确的;它们会更新引用计数,以管理Anchor本身的生命周期。否则,您如何知道何时释放Anchor自己的内存?此外,std :: shared_ptr确实实现了operator->;我不知道您是怎么得出这个想法的。 - Quuxplusone
STL是标准模板库的缩写,即容器、迭代器等。智能指针不属于其中。 - curiousguy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接