C++11 在 0 时uint64_t个循环的原子递减

c++11 atomic decrement of uint64_t cycles at 0

本文关键字:循环 uint64 C++11      更新时间:2023-10-16

我正在尝试编写一个简单的并行处理系统,该系统应该生成N个对象以放入多线程环境中的容器中。

为了告诉线程何时停止生成对象,我创建了一个简单的反向计数器,该计数器从 N 开始,从 0 开始,每个线程并行递减。计数器应该是uint64_t,我想尝试C++11的原子支持。代码如下所示

//Class member
std::atomic<uint_fast64_t> counter
//Parallel function
while(counter-- > 0)
{
do something
}

正确编译并执行,但它进入无限循环,因为一旦计数器达到 0,它就会进一步递减,但它会跳回到可用的最高整数,从而永远不会停止。

将类型更改为 int64 而不是 uint64 可以解决问题,但我想了解为什么我需要这种解决方法。

我目前的工作假设是,即使条件为假,递减也会完成,所以当第一个线程检查计数器为 0 时,它无论如何都会递减它,并且运算减法运算并不真正关心整数的编码,但执行简单的按位运算(我完全忘记了是哪一个,但我记得加法和减法是通过简单的按位异或和移位完成的),其中下一次迭代被解释为最大 uint 值。你觉得这个解释合理吗?

除了从 uint 切换到 int 之外,一个选项是将操作从递减切换到递增,但你能想到一种不同的算法来保持这种上下文中的递减吗?

编辑1

我想到的另一个可能的解决方案,即使不是特别优雅,是知道实际并行启动了多少线程,在起始值为 Tot+NThreads 的 N_Threads 处停止计数器

//In calling function
counter = Tot+NThreads
//Parallel function
while(counter-- > NThreads)
{
do something
}

原子性仅保证所有线程都看到一致的atomic值值。通常,像--这样的操作是读取-修改-写入操作。atomic仅保证没有其他线程修改计数器,而另一个线程正忙于修改计数器。

澄清一下:atomic防止数据竞争,没有别的。

假设两个线程,T1 和 T2 以及下面的 R、M、W 序列:现在线程 T2 的结果已被 T1 的结果覆盖,即计数器的值不一致

T1:读取 T2:读取 T2:修改

T1:修改 T2:写入 T1:写入

因此,在您的问题中,代码执行counter--这意味着无论其值如何,--都将始终完成。因此,如果该值已经为零,则现在将为 -1,或者在使用unsigned数据类型时,为 unsigned 类型的最大值。

你的假设

这或多或少是正确的。 (unsigned) 0 - 1将是最大的无符号整数。你的原子递减总是会发生的,即使条件是假的。

我们如何解决这个问题?

我相信您实际上正在寻找这样的东西:

std::atomic<uint_fast64_t> counter;
while (true) {
    uint_fast64_t cur = counter;
    if (cur == 0)
        break;
    if (counter.compare_exchange_strong(cur, cur - 1) == false)
        continue;
    ... // Perform work
} 

首先,我们测试计数器的当前值是否为 0。如果是这样,我们就完成了工作,我们应该退出。

如果它大于 0,那么我们需要递减计数器。这是一个比较和交换操作。因此,如果值在到达第二个原子操作所需的时间内没有变化,我们执行递减,然后做一些工作。如果我们被抢占了,那么我们只会再试一次。