不,我不这么认为。这不是旋转等待。它不是在等待另一个线程存储0 或其他东西。在lock cmpxchg 失败后立即重试确实有意义,而不是休眠约 100 个周期(在 Skylake 及更高版本上)或约 5 个周期(在早期的 Intel CPU 上)。
对于lock cmpxchg 完全完成(成功或失败)意味着缓存行现在在这个 核心上处于修改(或者可能只是独占?)状态,所以现在 是再试一次的最佳时机。
无锁原子的实际用例通常竞争激烈,否则您通常应该使用回退到操作系统辅助的睡眠/唤醒。
(但如果存在争用,locked 指令会进行硬件仲裁;在竞争激烈的情况下,我不知道核心是否有可能在输掉之前执行第二条 locked 指令再次缓存行。但希望是的。)
lock cmpxchg 不能虚假地失败,所以实际的活锁是不可能的:至少一个核心会通过让其 CAS 在这样的算法中成功而取得进展,因为每个核心都可以进行一轮。在 LL/SC 架构上,compare_exchange_weak 可能会虚假失败,因此到非 x86 的可移植性可能需要关心活锁,具体取决于实现细节,但我认为即便如此也不太可能。 (当然_mm_pause 仅适用于 x86。)
使用pause 的另一个原因是在离开旋转等待循环时避免内存顺序错误推测,该循环旋转只读等待在尝试原子声明之前看到锁已解锁。 (这比在 xchg 或 lock cmpxchg 上旋转并让所有等待的线程在缓存行上敲击要好。)
但这又不是问题,因为重试循环已经包含lock cmpxchg,这是一个完整的屏障以及原子 RMW,所以我认为这可以避免内存顺序错误推测。
特别是如果您有效/正确地编写循环以在重试时使用 cmpxchg 失败的加载结果,从循环中删除 var 的纯加载。
这是从 CAS 原语构造任意原子操作的规范方法。如果比较失败,compare_exchange_weak 会更新其第一个参数,因此您不需要在循环内再次加载。
#include <atomic>
int atomicLeftShift(std::atomic<int>& var, int shiftBy)
{
int expected = var.load(std::memory_order_relaxed);
int desired;
do {
desired = expected << shiftBy;
} while( !var.compare_exchange_weak(expected, desired) ); // seq_cst
return desired;
}
在 Godbolt 编译器资源管理器上使用 clang7.0 -O3 for x86-64 编译到这个 asm:
atomicLeftShift(std::atomic<int>&, int):
mov ecx, esi
mov eax, dword ptr [rdi] # pure load outside the loop
.LBB0_1: # do {
mov edx, eax
shl edx, cl # desired = expected << count
lock cmpxchg dword ptr [rdi], edx # eax = implicit expected, updated on failure
jne .LBB0_1 # } while(!CAS)
mov eax, edx # return value
ret
重试循环中唯一的内存访问是lock cmpxchg,它不会受到内存顺序错误推测的影响。因此,不需要pause。
对于简单的退避延迟也不需要pause,除非您有很多争用并且想让一个线程连续对同一个共享变量执行多项操作以提高吞吐量。即在 cmpxchg 失败的罕见情况下让其他线程退出。
仅如果一个线程在同一变量的一行中执行多个原子操作是正常的(或者如果您有错误共享问题,则在同一缓存行中执行一个)是正常的,而不是将更多操作放入一次 CAS 重试中。
这在实际代码中可能很少见,但在合成微基准测试中很常见,在这种情况下,您让多个线程反复敲击共享变量,中间没有其他工作。