如何实现不溢出的原子引用计数器?

时间:2016-06-28 13:53:56

标签: c++ multithreading atomic integer-overflow reference-counting

我正在考虑基于原子整数的引用计数,它可以避免溢出。怎么做?

请不要关注这种溢出是否是一个现实问题。即使没有实际重要性,任务本身也引起了我的兴趣。

示例

引用计数的示例实现在Boost.Atomic中作为示例显示。基于该示例,我们可以提取以下示例代码:

struct T
{
    boost::atomic<boost::uintmax_t> counter;
};

void add_reference(T* ptr)
{
    ptr->counter.fetch_add(1, boost::memory_order_relaxed);
}

void release_reference(T* ptr)
{
    if (ptr->counter.fetch_sub(1, boost::memory_order_release) == 1) {
        boost::atomic_thread_fence(boost::memory_order_acquire);
        delete ptr;
    }
}

另外,给出以下说明

  

始终可以使用memory_order_relaxed来增加引用计数器:对象的新引用只能从现有引用形成,并且将现有引用从一个线程传递到另一个线程必须已经提供任何所需的同步。 / p>      

在删除另一个线程中的对象之前,必须在一个线程(通过现有引用)强制执行对该对象的任何可能访问。这是通过&#34;发布&#34;删除引用后的操作(通过此引用对对象的任何访问必须明显发生在之前),并且&#34;获取&#34;删除对象之前的操作。

     

可以将memory_order_acq_rel用于fetch_sub操作,但这会导致不需要的&#34;获取&#34;当参考计数器尚未达到零并且可能会造成性能损失时的操作。

编辑&gt;&gt;&gt;

似乎Boost.Atomic文档在这里可能有误。毕竟可能需要acq_rel

至少这样是使用boost::shared_ptr完成时std::atomic的实现(还有其他实现)。请参阅文件boost/smart_ptr/detail/sp_counted_base_std_atomic.hpp

Herb Sutter也在他的演讲C++ and Beyond 2012: Herb Sutter - atomic<> Weapons, 2 of 2中提及它(引用计数部分从1:19:51开始)。在这次演讲中,他似乎也不鼓励使用栅栏。

感谢user 2501在下面的评论中指出了这一点。

&LT;&LT;&LT;结束编辑

初次尝试

现在的问题是写的add_reference可能(在某些时候)溢出。它会默默地这样做。当调用匹配的release_reference会过早地破坏对象时,这显然会导致问题。 (如果add_reference将再次被调用以达到1。)

我在想如何让add_reference检测溢出并优雅地失败,而不会冒任何风险。

一旦我们离开0,与fetch_add相比将无法做到,因为在这两者之间,其他一些线程可能再次调用add_reference(到达1)然后再release_reference (错误地破坏了有效的对象)。

先检查(使用load)也无济于事。这样,其他一些线程可以在我们对loadfetch_add的调用之间添加自己的引用。

这是解决方案吗?

然后我想也许我们可以从load开始,但只有在那时我们才compare_exchange

首先,我们执行load并获取本地值。如果是std::numeric_limits<boost::uintmax_t>::max()那么我们就会失败。 add_reference无法添加其他参考,因为已经采取了所有可能的参考。

否则我们会创建另一个本地值,即前一个本地引用计数加上1

现在我们compare_exchange提供原始本地引用计数的预期值(这可确保平均时间内没有其他线程修改引用计数),并且作为所需值增加的本地引用计数。

由于compare_exchange可能失败,我们必须在循环中执行此操作(包括load)。直到成功或检测到最大值。

一些问题

  1. 这样的解决方案是否正确?
  2. 要使其有效需要哪些内存排序?
  3. 应该使用哪个compare_exchange_weak_strong
  4. 会影响release_reference功能吗?
  5. 是否在实践中使用?

3 个答案:

答案 0 :(得分:2)

解决方案是正确的,也许可以用一件事来改进。目前,如果该值在本地CPU中达到最大值,则可以由另一个CPU减少,但当前CPU仍将缓存旧值。使用相同的compare_exchangeexpected进行虚拟newValue以确认最大值仍在那里然后再抛出异常(或任何你想要的东西)是值得的。

其余的:

无论您使用_weak还是_strong都无关紧要,因为无论如何它都会在循环中运行,因此下一个load将非常可靠地获得最新值。

对于add_referencerelease_reference - 谁会检查是否真的添加了?它会抛出异常吗?如果是的话,它可能会起作用。但通常最好允许这样的低级别事情不要失败,而是使用uintptr_t作为参考计数器,因此它永远不会溢出,因为它足以覆盖地址空间,因此同时存在任意数量的对象。

不,由于上述原因,它并未在实践中使用。

答案 1 :(得分:1)

快速数学:说uint是32位,所以max uint是4G(40亿东西)。每个引用/指针至少为4个字节(如果你在64位系统上,则为8个)所以为了溢出,你需要16Gbytes的内存专用于存储指向同一个对象的引用,这应该指向一个严重的设计缺陷。

我认为这不是今天的问题,也不是在可预见的未来。

答案 2 :(得分:1)

这个问题没有实际意义。即使假设原子增量需要1个CPU周期(它没有!),在4GHz CPU上需要半年时间来绕过64位整数,这样CPU就不会做任何事情,只能继续增加。

考虑到实际计划的实际情况,我发现很难相信这是一个可以纠缠你的真正问题。