C++ 原子抢占安全吗?

Are C++ atomics preemption safe?

根据我对原子的理解,它们是特殊的汇编指令,保证 SMP 系统中的两个处理器不能同时写入同一内​​存区域。例如,在 PowerPC 中,原子增量看起来像这样:

retry:
  lwarx  r4, 0, r3 // Read integer from RAM location r3 into r4, placing reservation.
  addi   r4, r4, 1 // Add 1 to r4.
  stwcx. r4, 0, r3 // Attempt to store incremented value back to RAM.
  bne-   retry     // If the store failed (unlikely), retry.

然而,这并不能保护这四个指令不被中断抢占,并且另一个任务被调度。为了防止抢占,您需要在输入代码之前进行中断锁定。

据我所见 C++ atomics,他们似乎在需要时强制执行锁定。所以我的第一个问题是 -

  1. C++标准能保证原子操作不会发生抢占吗?如果是这样,我可以在标准中的什么地方找到它?

我检查了我的 Intel PC 上的 atomic<int>::is_always_lock_free,结果是 true。根据我对上述装配块的假设,这让我感到困惑。在深入研究英特尔汇编(我不熟悉)之后,我发现 lock xadd DWORD PTR [rdx], eax 正在发生。所以我的问题是 -

  1. 是否有一些架构提供了保证无抢占的原子相关指令?还是我的理解有误?

最后我想知道 compare_exchange_weakcompare_exchange_strong 语义 -

  1. 是重试机制不同还是其他原因?

编辑:看完答案,我又好奇一件事

  1. 原子成员函数操作fetch_addoperator++等是强还是弱?

Does the C++ standard guarantee no preemption will happen during an atomic operation? If so, where in the standard can I find this?

不,不是。由于代码确实无法判断这是否发生(根据情况,它与原子操作之前或之后的抢占无法区分),因此没有理由这样做。

Do some architectures provide atomic related instructions which guarantee no-preemption? Or is my understanding wrong?

没有意义,因为该操作无论如何都必须看起来是原子的,因此期间的抢占在观察到的行为与之前或之后的抢占行为中总是相同的。如果您编写的代码曾经看到原子操作期间的抢占导致可观察到的效果不同于之前的抢占或之后的抢占,则该平台已损坏,因为该操作不是原子行为。

这类似于这个问题:Anything in std::atomic is wait-free?

以下是无锁和无等待的一些定义(均摘自Wikipedia):

An algorithm is lock-free if, when the program threads are run for a sufficiently long time, at least one of the threads makes progress.

An algorithm is wait-free if every operation has a bound on the number of steps the algorithm will take before the operation completes.

你的带有重试循环的代码是无锁的:如果存储失败,一个线程只需要执行重试,但这意味着该值必须同时更新,所以其他线程必须进步。

关于无锁,线程是否可以在原子操作的中间被抢占并不重要。

某些操作可以转换为单个原子操作,在这种情况下,此操作是 无等待,因此不能中途被抢占。但是,哪些操作实际上是无等待的取决于编译器和目标体系结构(如我在引用的 SO 问题中的回答中所述)。

关于 compare_exchange_weakcompare_exchange_strong 之间的区别 - 弱版本可能会虚假地失败,即,即使比较实际上是正确的,它也可能会失败。这可能发生在 LL/SC 的架构上。假设我们使用 compare_exchange_weak 来更新一些具有期望值 A 的变量。 LL从变量中加载值A,在SC执行前,变量变为B,然后变回 ]A。因此,即使变量包含与以前相同的值,B 的中间更改也会导致 SC(因此 compare_exchange_weak)失败。 compare_exchange_strong 不能虚假地失败,但要实现它必须在具有 LL/SC.

的体系结构上使用重试循环

我不完全确定fetch_add“强或弱”是什么意思。 fetch_add 不能 失败 - 它只是通过添加提供的值和 returns 变量的旧值来执行一些变量的原子更新。这是否可以转换为单个指令(如 Intel)或使用 LL/SC(Power)或 CAS(Sparc)的重试循环取决于目标架构。无论哪种方式,都保证变量被正确更新。