如何在C++中实现线程安全的引用计数

21

如何在C++编程语言中实现一个高效且线程安全的引用计数系统?

我经常遇到一个问题,即关键操作不是原子的,而且可用的X86互锁操作不足以实现引用计数系统。

下面的文章涵盖了这个主题,但需要特殊的CPU指令:

http://www.ddj.com/architect/184401888

7个回答

13
现在,您可以使用Boost/TR1 shared_ptr<>智能指针来保持您的引用计数引用。
非常好用,不需要额外的麻烦。shared_ptr<>类负责所有引用计数所需的锁定。

11
这个回答要么是我理解错了,要么有问题...如果它甚至不能回答原来的问题,只是建议使用已经实现了同样功能的东西,那为什么它会被接受并赞同这么多次呢? - Paulius
警告:boost::shared_ptr在所有关键操作上都不是线程安全的。例如,在交换时它不是线程安全的。 - Catskul
1
@Catskul:这在文档中已经记录为“ shared_ptr对象提供与内置类型相同级别的线程安全性”:http://www.boost.org/doc/libs/1_52_0/libs/smart_ptr/shared_ptr.htm#ThreadSafety。虽然您不能同时从多个线程修改同一`shared_ptr`实例,但修改共享对象所有权的不同`shared_ptr`的操作是没有线程安全问题的。 - Michael Burr
@Michael-Burr 这并不意味着所有关键操作都是原子性的。要使原子性相关,必须存在一个点,在该点上其中一个shared_ptr在多个线程中使用。在这些情况下可能使用的某些操作不是原子性的,因此需要注意。 - Catskul
1
@Catskul: shared pointers的使用场景是有不同实例的shared pointer指向同一个对象。在不同线程中修改这些shared pointer是线程安全的(尽管从多个线程使用指向的对象并不一定如此)。Shared pointers解决了管理共享对象生命期的问题。它们并不解决所有多线程问题,包括从多个线程修改特定shared pointer实例的问题。 - Michael Burr
显示剩余4条评论

6

在VC++中,您可以使用_InterlockedCompareExchange函数。

do
   read the count
   perform mathematical operation
   interlockedcompareexchange( destination, updated count, old count)
until the interlockedcompareexchange returns the success code.

在其他平台/编译器上,使用适当的内置函数来执行LOCK CMPXCHG指令,这与MS的_InterlockedCompareExchange函数相对应。


1
或者简单地使用InterlockedIncrement/Decrement。 - yrp
@yrp: 它比do while循环/比较和交换快吗?据我所知,InterlockedIncrement/Decrement会发出LOCK INC/LOCK DEC x86指令,而cas则不会(至少对于这个指令来说,LOCK是隐含的)。 - elmattic

3

严格来说,要想在纯C++中编写线程安全的代码,您需要等到C++0x版本。

目前,您可以使用Posix,或者创建自己的平台无关包装器,围绕比较和交换以及/或者原子增量/减量进行操作。


1
或者像其他人一样使用 Boost::TR1。 - gnud
如果您不关心Win32,那么在我看来,Posix是最好的选择(快速且简单)。当然,现在主要的编译器都支持C++0x,这意味着您可以使用TR1(它是C++11标准的一部分)。您只需要打开选项(C++0x编译器标志)即可。 - Pijusn

2

如果你想要安全并且在可能重新排序的平台上关注,因此需要同时发出内存屏障,那么Win32 InterlockedIncrementAcquire和InterlockedDecrementRelease将是原子操作,并且可以完成工作。如果你确定会保持x86,则可以使用InterlockedIncrement和InterlockedDecrement。

话虽如此,Boost/TR1 shared_ptr<>将为你处理所有这些问题,因此除非你需要自己实现它,否则最好坚持使用它。


1
请记住,锁定非常昂贵,并且每次在智能指针之间传递对象时都会发生 - 即使对象当前由一个线程拥有(智能指针库不知道这一点)。
鉴于此,可能适用于此处的经验法则(我很乐意纠正!)
如果以下情况适用于您:
- 您具有复杂的数据结构,编写析构函数将很困难(或者按设计使用STL样式值语义将不合适),因此需要智能指针来为您完成此操作,并且 - 您正在使用共享这些对象的多个线程,并且 - 您关心性能以及正确性
那么实际垃圾回收可能是更好的选择。尽管GC在性能方面声名狼藉,但这是相对的。我认为它与锁定智能指针相比非常有优势。这是CLR团队选择真正的GC而不是使用引用计数的原因之一。请参见this article,特别是这种引用赋值方式的明显比较: 无引用计数:
a = b;

引用计数:

if (a != null)
    if (InterlockedDecrement(ref a.m_ref) == 0)
            a.FinalRelease();

if (b != null)
    InterlockedIncrement(ref b.m_ref);

a = b;

当另一个线程在InterlockedDecrement(...)a.FinalRelease();之间引用时会发生什么? - smokku
这在设计中是不可能的-这是生成的代码,而不是手写的。应用程序代码只是说a = b。如果两个线程有足够的权限访问对象以增加计数,则计数已经> = 2。任何一个都可以将其减少,但这样做同时放弃了增加它的能力。 - Daniel Earwicker
当另一个线程在InterlockedDecrement(...)a.FinalRelease();之间执行a = b会发生什么? - smokku

0

如果指令本身不是原子的,那么您需要将更新适当变量的代码部分设置为临界区。

,您需要使用某些锁定方案防止其他线程进入该代码部分。当然,锁必须是原子的,但您可以在pthread_mutex类中找到原子锁定机制。

效率问题:pthread库尽可能高效,同时保证互斥锁对于您的操作系统是原子的。

是否昂贵:可能。但对于需要保证的所有内容都有一个成本。


0

那篇DDJ文章中发布的特定代码为解决使用智能指针时出现的错误增加了额外的复杂性。

具体来说,如果您无法保证智能指针在分配给另一个智能指针时不会更改,则您正在做错事情或者一开始就在做非常不可靠的事情。如果智能指针在被分配给另一个智能指针时可以更改,这意味着执行分配操作的代码并不拥有该智能指针,这本身就是可疑的。


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