weak_ptr 是如何工作的?
我了解如何使用 weak_ptr
和 shared_ptr
。通过计算其对象中的引用数量,我了解了 shared_ptr
的工作原理。 weak_ptr
是如何工作的?我尝试阅读 boost 源代码,但我对 boost 不够熟悉,无法理解它使用的所有内容。
谢谢。
I understand how to use weak_ptr
and shared_ptr
. I understand how shared_ptr
works, by counting the number of references in its object. How does weak_ptr
work? I tried reading through the boost source code, and I'm not familiar enough with boost to understand all the things it uses.
Thanks.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
shared_ptr
使用额外的“计数器”对象(又名“共享计数”或“控制块”)来存储引用计数。(顺便说一句:“计数器”对象还存储删除器。)
每个
shared_ptr
和weak_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_ptr也引用了计数器对象,因此它们也必须增加“弱计数”。
一个可能更直观的解决方案是增加每个
shared_ptr
的“弱计数”,因为每个shared_ptr
都保存对“计数器”对象的引用。为所有
shared_ptr
实例添加一个只是一种优化(在复制/分配shared_ptr
实例时保存一个原子增量/减量)。shared_ptr
uses an extra "counter" object (aka. "shared count" or "control block") to store the reference count.(BTW: that "counter" object also stores the deleter.)
Every
shared_ptr
andweak_ptr
contains a pointer to the actual pointee, and a second pointer to the "counter" object.To implement
weak_ptr
, the "counter" object stores two different counters:shared_ptr
instances pointing to the object.weak_ptr
instances pointing to the object, plus one if the "use count" is still > 0.The pointee is deleted when the "use count" reaches zero.
The "counter" helper object is deleted when the "weak count" reaches zero (which means the "use count" must also be zero, see above).
When you try to obtain a
shared_ptr
from aweak_ptr
, the library atomically checks the "use count", and if it's > 0 increments it. If that succeeds you get yourshared_ptr
. If the "use count" was already zero you get an emptyshared_ptr
instance instead.EDIT: Now, why do they add one to the weak count instead of just releasing the "counter" object when both counts drop to zero? Good question.
The alternative would be to delete the "counter" object when both the "use count" and the "weak count" drop to zero. Here's the first reason: Checking two (pointer sized) counters atomically is not possible on every platform, and even where it is, it's more complicated than checking just one counter.
Another reason is that the deleter must stay valid until it has finished executing. Since the deleter is stored in the "counter" object, that means the "counter" object must stay valid. Consider what could happen if there is one
shared_ptr
and oneweak_ptr
to some object, and they are reset at the same time in concurrent threads. Let's say theshared_ptr
comes first. It decreases the "use count" to zero, and begins executing the deleter. Now theweak_ptr
decreases the "weak count" to zero, and finds the "use count" is zero as well. So it deletes the "counter" object, and with it the deleter. While the deleter is still running.Of course there would be different ways to assure that the "counter" object stays alive, but I think increasing the "weak count" by one is a very elegant and intuitive solution. The "weak count" becomes the reference count for the "counter" object. And since
shared_ptr
s reference the counter object too, they too have to increment the "weak count".A probably even more intuitive solution would be to increment the "weak count" for every single
shared_ptr
, since every singleshared_ptr
hold's a reference to the "counter" object.Adding one for all
shared_ptr
instances is just an optimization (saves one atomic increment/decrement when copying/assigningshared_ptr
instances).基本上,“weak_ptr”是一个普通的“T*”指针,可让您在代码后面恢复强引用,即“shared_ptr”。
就像普通的 T* 一样,weak_ptr 不执行任何引用计数。在内部,为了支持任意类型 T 上的引用计数,STL(或实现此类逻辑的任何其他库)创建一个我们称为“Anchor”的包装对象。 “Anchor”的存在只是为了实现我们需要的引用计数和“当计数为零时,调用删除”行为。
在强引用中,shared_ptr 实现其复制、operator=、构造函数、析构函数和其他相关 API 来更新“Anchor”的引用计数。这就是shared_ptr如何确保你的“T”在有人使用它的时候一直存在。在“weak_ptr”中,这些相同的 API 只是复制实际的 Anchor ptr。他们不更新引用计数。
这就是为什么“weak_ptr”最重要的API是“过期”和名字不好的“锁”。 “过期”告诉您底层对象是否仍然存在,即“它是否已经删除自身,因为所有强引用都超出了范围?”。 “Lock”将(如果可以的话)将weak_ptr转换为强引用shared_ptr,恢复引用计数。
顺便说一句,“锁”对于该 API 来说是一个糟糕的名字。您不是(只是)调用互斥体,而是通过“锚定”行为从弱互斥体创建强引用。这两个模板中最大的缺陷是它们没有实现运算符->,因此要对对象执行任何操作,您必须恢复原始“T*”。他们这样做主要是为了支持“shared_ptr”之类的东西,因为原始类型不支持“->”操作员。
Basically, a "weak_ptr" is an ordinary "T*" pointer that lets you RECOVER a strong reference, i.e. "shared_ptr", later in the code.
Just like an ordinary T*, the weak_ptr doesn't do any reference-counting. Internally, to support reference-counting on an arbitrary type T, the STL (or any other library implementing this kind of logic) creates a wrapper object we'll call "Anchor". "Anchor" exists solely to implement the reference count and "when count is zero, call delete" behavior we need.
In a strong reference, the shared_ptr implements its copy, operator=, constructor, destructor, and other pertinent APIs to update "Anchor"'s reference count. This is how a shared_ptr ensures that your "T" lives for exactly as long as somebody is using it. In a "weak_ptr", those same APIs just copy the actual Anchor ptr around. They do NOT update reference counts.
This is why the most important APIs of "weak_ptr" are "expired" and the poorly-named "lock". "Expired" tells you if the underlying object is still around- i.e. "Has it already deleted itself because all strong references went out of scope?". "Lock" will (if it can) convert the weak_ptr to a strong reference shared_ptr, restoring reference-counting.
BTW, "lock" is a terrible name for that API. You aren't (just) invoking a mutex, you're creating a strong reference from a weak one, with that "Anchor" acting. The biggest flaw in both templates is that they did not implement operator->, so to do anything with your object you have to recover the raw "T*". They did this mostly to support things like "shared_ptr", because primitive types don't support the "->" operator.