引用计数实现起来,真是太难了

引用计数实现起来,真是太难了

作者:BlogUpdater |  时间:2021-01-26 |  浏览:780 |  评论已关闭 条评论

使用引用计数进行对象生命周期管理
使用托管代码的一个好处是:你不需要关心对象的生命周期管理工作。
下面的一个例子展示了在非托管代码中是如何进行引用计数管理的,并且代码中还有一些小Bug。让我们先来看看代码。

上述代码的想法是:将一个指向对象的指针替换为指向另一个对象。这种技法存在于许多”智能指针”的实现中,下面是使用这段代码的例子:

那么,我为什么要解释这个呢?因为这样你就可以明白智能指针的工作原理了。
但是,上面说的Bug是什么意思呢?
从这里开始,停止往下看,先开动小脑筋思考一下,上面的代码到底有没有问题,有什么问题?

问题出在哪里?
旧的对象在新对象的AddRef调用之前就执行了Release。考虑下面的代码:

上述代码中的赋值操作看起来再正常不过了。但是,真的这样吗?

智能指针初始化为NULL,然后CreateStream创建了一个流对象并赋值给了一个智能指针。
流对象的引用计数为1。然后,当执行下列语句时:

在SetObject函数内部,ppt指向了tp spstm.m_p,而pptNew等效于spstm.m_p的原始值。

SetObject函数做的第一件事,就是释放掉旧的指针,这个操作会将流对象的引用计数减为0。
这会直接导致流对象的销毁。然后ptNew参数(现在指向的是一个已经被释放的对象)被赋值给了spstm.m_p,最后,ptNew指针(还是指向一个已经被释放的对象)会执行AddRef。
啊哦,我们正在一个被释放了的对象上执行方法,这肯定不是什么好事儿。

如果你足够幸运的话,AddRef会就地崩溃,而你也很容易通过调试找到问题的原因。
如果你运气不好的话,AddRef调用会认为被释放的内存还是有效的,然后增加这一内存块的引用计数。恭喜,你成功的破坏了内存中的数据。如果这还不足以引发一次崩溃,当智能指针离开它的作用域或者被它的引用者所修改,则无效的m_p指针会执行Release,从而再一次破坏内存数据。

这就是为什么智能指针的赋值操作需要在Release旧指针之前需要AddRef的原因,如下图所示:

如果你有机会看一下ATL模板库中的AtlComPtrAssign函数的话,则你会发现,它的实现和上图中的是一样的。

总结
涉及到指针的赋值的操作,隐含的一层意思是:被指向对象的控制权发生了转移。
如果对象的生命周期是由引用计数实现,则在指针的赋值操作时,需要特别注意引用计数的更新顺序。

最后
Raymond Chen的《The Old New Thing》是我非常喜欢的博客之一,里面有很多关于Windows的小知识,对于广大Windows平台开发者来说,确实十分有帮助。
本文来自:《Reference counting is hard.》

评论已关闭。