例如,您可以安全地递增和递减 std::atomic_int 。但是,如果您需要检查溢出或根据该值有条件地执行某些例程,那么无论如何都需要锁。由于您必须比较该值,并且在比较成功后线程可能会被交换,另一个线程会修改,...bug。
但是如果你需要锁,那么你可以使用普通整数而不是原子。我说得对吗?
不,你仍然可以有条件地使用 std::atomic 。
首先,
.fetch_add
的溢出行为被明确定义为原子类型的无符号或2的补码环绕(尽管可能不是您想要的)。与非原子类型不同,整数溢出仍然是 UB。
你是对的,
foo++
然后单独读取foo
将是一个错误,所以使用int tmp = foo++;
来使用原子RMW操作的返回值。
如果您绝对必须检查溢出,或以其他方式有条件地执行操作,则可以使用比较交换。这使您可以读取该值,决定是否要对其进行处理,然后自动更新该值(如果它没有更改)。这里的关键部分是系统会告诉你原子更新是否失败,在这种情况下你可以回到开始并读取新值并再次做出决定。
举个例子,如果我们只想将原子整数的最大值设置为 4(例如,在某种引用计数中),我们可以这样做:
#include <atomic>
static std::atomic<int> refcount = 0;
int val = refcount; // this doesn't need to be in the loop as on failure compare-exchange-strong updates it
while(true)
{
if(val == 4)
{
// there's already 4 refs here, maybe come back later?
break;
}
int toChangeTo = val + 1;
if(refcount.compare_exchange_weak(val, toChangeTo))
{
// we successfully took a ref!
break;
}
// if we fail here, another thread updated the value whilst we were running, just loop back and try again
}
这样的循环通常写为
do{...}while(!refcount.compare_exchange_weak(...));
我们使用
compare_exchange_weak
代替 compare_exchange_strong
。有时这可能会错误地失败,因此您需要循环执行此操作。然而,无论如何我们都有一个循环(一般来说,当你需要处理真正的失败时,你总是会这样做),所以compare_exchange_weak
在这里很有意义。
在某些 ISA 上,需要一个 asm 循环来实现
compare_exchange_strong
,但不需要 weak
;在 x86 等其他 ISA 上,两者都编译为相同的 asm。只要准备下一次 weak
尝试的代码像本例中那样便宜,就不会使用 compare_exchange_weak
使任何事情变得更糟。