如何实现不会溢出的原子参考计数器

How to implement atomic reference counter that does not overflow?

本文关键字:溢出 计数器 参考 何实现 实现      更新时间:2023-10-16

我正在考虑基于原子整数的参考计数,这些原子可以免受溢出的影响。如何做?

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


示例

参考计数的示例实现在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来增加参考计数器:对对象的新引用只能从现有引用中形成,并且将现有参考从一个线程传递到另一个线程必须已经提供任何必需的同步。

在一个线程中(通过现有引用)在删除其他线程中删除对象之前实施对对象的任何可能访问。这是通过删除参考后的"释放"操作来实现的(通过此引用显然必须发生对对象的任何访问),然后在删除对象之前进行"获取"操作。

可以将memory_order_acq_rel用于fetch_sub操作,但是当参考计数器尚未达到零时,这会导致不需要的"获取"操作,并且可能施加性能罚款。

编辑>>>

看来BOOST.ATOMIC文档在这里可能是错误的。毕竟可能需要acq_rel

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

Herb Sutter还在他的演讲C 和2012年以后提到了这一点:Herb Sutter -Atomic&lt;>武器,第2个(共2个)(参考计数部分开始于1:19:51)。另外,他似乎不鼓励在这次演讲中使用围栏。

感谢用户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. 它是在实践中使用的吗?

解决方案是正确的,也许可以用一件事对其进行改进。当前,如果该值在本地CPU中达到最大值,则可以通过另一个CPU将其降低,但是当前的CPU仍将缓存旧值。使用相同的expectednewValue进行虚拟compare_exchange值得一试

其余的:

无论您使用_weak还是_strong,无论如何它都会在循环中运行,因此下一个load将获得最新值。

对于add_referencerelease_reference-然后谁将检查它是否真的添加了?会引发例外吗?如果是,它可能会起作用。但是通常,最好允许这样的低级别的事情不要失败,而是将UINTPTR_T用于参考计数器,因此它永远不会溢出,因为它足够大以覆盖地址空间,因此同时存在的任何数量的对象。p>否,由于上述原因而在实践中不使用它。

快速数学:说uint是32位,所以Max Uint是4G(4十亿美元)。每个参考/指针至少为4个字节(如果您在64位系统上),那么要溢出,您需要16Gbytes的存储器,用于存储指向同一对象的引用,这应该指向一个严重的Deign缺陷。

我会说这不是今天的问题,也不是在可预见的将来。

这个问题是没有问题的。即使假设原子增量需要1个CPU周期(不!),在4GHz CPU上,将需要半年的时间来包裹64位整数,提供CPU除了继续增量外别无其他。

考虑到实际程序的现实,我很难相信这是一个真正的问题。