【问题标题】:Are C++ atomics preemption safe?C++ 原子抢占安全吗?
【发布时间】:2020-06-21 04:11:16
【问题描述】:

根据我对原子的理解,它们是特殊的汇编指令,可保证 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++ 标准是否保证在原子操作期间不会发生抢占?如果是这样,我可以在标准中的哪个位置找到它?

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

  1. 某些体系结构是否提供保证不抢占的原子相关指令?还是我的理解有误?

最后我想知道 compare_exchange_weakcompare_exchange_strong 语义 -

  1. 区别在于重试机制还是其他原因?

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

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

【问题讨论】:

  • 无锁不保证不可抢占。您引用的 PowerPC 示例是无锁的,但仍然是可抢占的。 (1) C++ 不保证不可抢占的行为。正如您所指出的,某些处理器支持非抢占式读取-修改-写入操作,但并非全部,例如 PowerPC。 (2) 是的,一些架构确实支持非抢占式读-修改-写原子。 (3) 不同的是,weak 允许虚假失败。这符合 PowerPC 语义。
  • 唯一的“弱”原子是compare_exchange_weak。其他人无法发出失败信号,并且具有无条件的效果。在像 PowerPC 这样的 LL/SC 机器上,它们必须编译成像 CAS_strong 这样的重试循环。 (当然纯加载/纯存储除外,因为它们不是 RMW 操作,所以不需要重试循环即可。)

标签: c++ atomic stdatomic


【解决方案1】:

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

下面是 lock-freedom 和 wait-freedom 的一些定义(均取自 Wikipedia):

如果当程序线程运行足够长的时间后,至少有一个线程取得进展,则算法是无锁

如果每个操作都限制了算法在操作完成之前将采取的步数,则该算法是无需等待

您的带有重试循环的代码是无锁的:一个线程只需要在存储失败时执行重试,但这意味着该值必须同时更新,因此其他线程必须取得进展。

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

某些操作可以转换为单个原子操作,在这种情况下,此操作是无需等待的,因此不能在中途被抢占。但是,哪些操作实际上无需等待取决于编译器和目标架构(如我在引用的 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 不能失败 - 它只是通过添加提供的值来执行某个变量的原子更新,并返回变量的旧值。是否可以将其转换为单个指令(如在 Intel 上)或转换为具有 LL/SC(Power)或 CAS(Sparc)的重试循环取决于目标体系结构。无论哪种方式,都可以保证正确更新变量。

【讨论】:

  • en.wikipedia.org/wiki/Load-link/store-conditional 表示,某些 LL/SC 实现可能会在修改同一块(某些大小)中的 附近 数据时虚假失败。 IDK 如果现实世界的 PowerPC 监控总是精确到缓存行。但是,是的,一定有一些事情取得了进展,即使它是一个单独的线程,它与我们共享的东西错误地共享了它的一些私有数据。
【解决方案2】:

C++ 标准是否保证在原子操作期间不会发生抢占?如果是这样,我可以在标准中的哪个位置找到它?

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

某些架构是否提供保证不抢占的原子相关指令?还是我的理解有误?

没有意义,因为无论如何操作必须看起来是原子的,所以在观察到的行为中的抢占总是与之前或之后的抢占相同。如果您编写的代码曾经遇到过在原子操作期间的抢占导致可观察到的效果不同于之前的抢占或之后的抢占的情况,那么该平台就会被破坏,因为该操作的行为不是原子的。

【讨论】:

  • 在非无锁原子操作期间的抢占将是一件大事。 (如果需要相同的锁,则阻塞其他线程,直到该线程再次获得 CPU 时间)。所以是的,从这个意义上说,一些 ISA 确实提供了无锁原子。在某些 ISA(如 x86 或 ARMv8.1)上,存在无法中断的单指令 RMW;中断发生与否,不会导致虚假的 LL/SC 故障。但正如你所说,这种区别并不重要。一旦原子是无锁的,你就没事了。由中断引起的虚假 LL/SC 故障与其他原因没有明显的不同。
  • 问题不在于是否可以抢占它们,而是它们是否保证不被抢占。考虑到您无论如何都无法区分,这对于平台来说是一个奇怪的保证。
  • 我试图说明这一点,如果您使用atomic<T> 使得is_always_lock_free 为假,那么您可以可能会分辨出不同的性能。 (与一个假设的实现相比,在持有锁哈希表中的一个锁时永远不能进行上下文切换)。您的答案只考虑了无锁情况,这很好,因为非无锁原子很糟糕,而且大多只存在,所以 C++ 可以定义一个可移植的标准,而不是你可以实际使用它们。
  • @PeterCordes 表现不佳可能来自任何方面。程序无法判断它是在持有锁时被抢占的。而且,无论如何,每个理智的平台都会竭尽全力让您在该平台上获得最佳性能。如果性能不好,那将是因为平台无法做得更好,而不是因为某些特定的实现错误或保证。 (除非 atomic 本身的实现不好,当然核心功能实现不好的解决方案是使用更好的实现。)
  • 你可以设计一个实验来展示效果:让一堆线程都读取一个原子对象。了解性能如何随线程数量与逻辑 CPU 内核数量的关系:一旦并非所有线程都可以始终运行,如果吞吐量下降幅度大于预期,则可能是由于关键部分内发生了一些休眠。 (当然你不能做出绝对的陈述,我并不反对这一点)。
猜你喜欢
  • 1970-01-01
  • 2011-08-19
  • 2020-05-13
  • 2018-09-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多