忙等待循环中是否需要内存屏障或原子操作?

gav*_*avv 21 c++ multithreading gcc spinlock memory-barriers

考虑以下spin_lock()实现,最初来自这个答案:

void spin_lock(volatile bool* lock)  {  
    for (;;) {
        // inserts an acquire memory barrier and a compiler barrier
        if (!__atomic_test_and_set(lock, __ATOMIC_ACQUIRE))
            return;

        while (*lock)  // no barriers; is it OK?
            cpu_relax();
    }
}
Run Code Online (Sandbox Code Playgroud)

我所知道的:

  • volatile防止编译器*lockwhile循环的每次迭代中优化重新读取;
  • volatile 不插入内存或编译器障碍 ;
  • 这样的实现实际上可以在GCC中工作x86(例如在Linux内核中)和其他一些架构;
  • 至少一个存储器和编译器屏障需要spin_lock()执行针对通用体系结构; 这个例子插入它们__atomic_test_and_set().

问题:

  1. volatile这里是否足够或者是否存在while循环中需要内存或编译器障碍或原子操作的架构或编译器?

    1.1根据C++标准?

    1.2在实践中,对于已知的体系结构和编译器,特别是它支持的GCC和平台?

  2. 在GCC和Linux支持的所有体系结构上,此实现是否安全?(在某些架构上至少效率低下,对吧?)
  3. while根据C++11它的内存模型,循环是否安全?

有几个相关的问题,但我无法从它们构建一个明确和明确的答案:

Die*_*ühl 13

这很重要:在C++ volatile没有任何与并发性有关的东西!目的volatile是告诉编译器它不应优化对受影响对象的访问.它没有告诉CPU什么,主要是因为CPU已经知道内存是否存在volatile.目的volatile是有效地处理内存映射I/O.

C++标准在1.10节[intro.multithread]中非常清楚,对在一个线程中修改并在另一个线程中访问(修改或读取)的对象的非同步访问是未定义的行为.避免未定义行为的同步原语是库组件,如原子类或互斥体.该子句volatile仅在信号(即as volatile sigatomic_t)的上下文中以及在前进进程的上下文中提及(即,线程最终将执行具有可观察效果的操作,如访问volatile对象或执行I/O).没有提到volatile与同步相关联.

因此,对跨线程共享的变量进行不同步评估会导致未定义的行为.是否声明volatile对此未定义的行为无关紧要.


dav*_*mac 5

维基百科关于内存屏障的页面

...其他体系结构,例如 Itanium,提供单独的“获取”和“释放”内存屏障,分别从读取器(接收器)或写入器(源)的角度解决写后读操作的可见性.

对我来说,这意味着 Itanium 需要一个合适的栅栏来使其他处理器可以看到读/写,但这实际上可能只是为了排序。我认为,这个问题实际上归结为:

是否存在一种架构,如果没有指示,处理器可能永远不会更新其本地缓存?我不知道答案,但如果你以这种形式提出问题,那么其他人可能会提出这个问题。在这样的架构中,您的代码可能会进入一个无限循环,其中读取的*lock总是看到相同的值。

就一般 C++ 合法性而言,示例中的一个原子测试和设置是不够的,因为它仅实现了一个栅栏,这将允许您*lock在进入 while 循环时看到的初始状态,但不能看到它何时更改(这会导致未定义的行为,因为您正在读取在另一个线程中未经同步更改的变量)-因此您的问题 (1.1/3) 的答案是否定的

另一方面,在实践中,(1.2/2) 的答案是肯定的(考虑到GCC 的 volatile 语义),只要架构保证缓存一致性而没有显式内存栅栏,这对于 x86 和可能对于许多架构都是如此,但我对于 GCC 支持的所有架构是否正确,无法给出明确的答案。然而,故意依赖根据语言规范在技术上是未定义行为的代码的特定行为通常是不明智的,特别是如果不这样做就可以获得相同的结果。

顺便说一句,鉴于memory_order_relaxed存在,在这种情况下似乎没有理由不使用它,而不是尝试通过使用非原子读取进行手动优化,即将示例中的 while 循环更改为:

    while (atomic_load_explicit(lock, memory_order_relaxed)) {
        cpu_relax();
    }
Run Code Online (Sandbox Code Playgroud)

例如,在 x86_64 上,原子负载成为常规mov指令,优化后的汇编输出与原始示例的输出基本相同。

  • 因为“易失性”通常被认为意味着该地址可能指的是其值由硬件控制的位置(即内存映射 IO 地址),所以我认为您可以相当确定 GCC 和其他编译器会执行您的操作想要这里。但是请参阅修改后的答案 - 当您可以使用带有 `memory_order_relaxed` 的原子负载时,实际上没有必要依赖于此。 (2认同)

mks*_*eve 1

  1. 这里是否足够易失性,或者是否有任何架构或编译器在 while 循环中需要内存或编译器屏障或原子操作?

易失性代码会看到变化吗?是的,但不一定像存在记忆障碍那样快。在某些时候,会发生某种形式的同步,并且将从变量中读取新状态,但无法保证代码中其他地方发生了多少情况。

1.1 根据C++标准?

来自cppreference :内存顺序

内存模型和内存顺序定义了代码需要处理的通用硬件。为了使消息在执行线程之间传递,需要发生线程间发生之前关系。这需要...

  • A 与 B 同步
  • A 在 B 之前有一个 std::atomic 操作
  • A 间接与 B 同步(通过 X)。
  • A 在 X 之前排序,线程间在 B 之前发生
  • A 线程间发生在 X 之前,X 线程间发生在 B 之前。

由于您没有执行任何这些情况,因此您的程序在某些当前硬件上可能会失败。

实际上,时间片的结束将导致内存变得一致,或者非自旋锁线程上的任何形式的屏障将确保刷新缓存。

不确定易失性读取获取“当前值”的原因。

1.2 在实践中,对于已知的架构和编译器,特别是对于 GCC 及其支持的平台?

由于该代码与通用 CPU 不一致,因此从那时起,C++11该代码很可能无法在尝试遵守该标准的 C++ 版本中执行。

来自cppreference : const 易失性限定符 易失性访问会阻止优化将工作从之前移动到之后,以及从之后移动到之前。

“这使得易失性对象适合与信号处理程序通信,但不适合与另一个执行线程通信”

因此,实现必须确保从内存位置而不是任何本地副本读取指令。但它不必确保通过缓存刷新易失性写入,以在所有 CPU 上生成一致的视图。从这个意义上说,写入 易失性变量后多长时间对另一个线程可见是没有时间限制的。

另请参阅kernel.org 为什么内核中的 volatile 几乎总是错误的

此实现在 GCC 和 Linux 支持的所有架构上是否安全?(至少在某些架构上效率很低,对吧?)

无法保证易失性消息会脱离设置它的线程。所以不太安全。在linux上可能是安全的。

根据 C++11 及其内存模型,while 循环安全吗?

否 - 因为它不会创建任何线程间消息传递原语。


归档时间:

查看次数:

1924 次

最近记录:

7 年,11 月 前