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

时间:2015-09-20 09:10:42

标签: c++ multithreading gcc memory-barriers spinlock

考虑以下spin_lock()实施,最初来自this answer

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();
    }
}

我已经知道的事情:

问题:

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

    1.1根据C++标准?

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

  2. GCC和Linux支持的所有架构上的此实现是否安全? (在某些架构上至少效率低下,对吗?)
  3. 根据while及其内存模型,C++11循环是否安全?
  4. 有几个相关的问题,但我无法从中提出明确而明确的答案:

3 个答案:

答案 0 :(得分:13)

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

C ++标准在第1.10节[intro.multithread]中非常清楚,对在一个线程中修改并在另一个线程中访问(修改或读取)的对象的非同步访问是未定义的行为。避免未定义行为的同步原语是库组件,如原子类或互斥体。该条款仅在信号的上下文(即volatile)和前向进展的上下文中提及volatile sigatomic_t(即,线程最终会做一些具有可观察效果的事情,如访问{ {1}}对象或做I / O)。没有提到volatile与同步相结合。

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

答案 1 :(得分:4)

来自Wikipedia page on memory barriers

  

......其他架构,例如Itanium,提供单独的"获取"和"发布"内存障碍,分别从读者(接收器)或写入器(源)的角度解决了读写后操作的可见性。

对我而言,这意味着Itanium需要一个合适的栅栏来使读/写对其他处理器可见,但实际上这可能只是为了订购的目的。我认为,这个问题归结为:

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

就一般的C ++合法性而言,你的例子中的一个原子测试和设置是不够的,因为它只实现了一个单独的围栏,可以让你看到*lock的初始状态。进入while循环,但没有看到它何时发生变化(这会导致未定义的行为,因为你正在读取一个在没有同步的情况下在另一个线程中被更改的变量) - 所以你的问题(1.1 / 3)的答案是

另一方面,在实践中,(1.2 / 2)的答案是肯定的(给定GCC's volatile semantics),只要该体系结构保证缓存一致性而没有明确的内存防护,这对x86来说是正确的对于许多架构而言,我无法就GCC支持的所有架构是否属实做出明确的答案。然而,根据语言规范故意依赖技术上未定义的行为的特定代码行为通常是不明智的,特别是如果可以在不这样做的情况下获得相同的结果。

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

    while (atomic_load_explicit(lock, memory_order_relaxed)) {
        cpu_relax();
    }

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

答案 2 :(得分:1)

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

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

  

1.1根据C ++标准?

来自cppreference : memory_order

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

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

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

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

不确定挥发性读取的原因是否获得&#34;当前值&#34;。

  

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

由于代码与通用CPU不一致,从C++11开始,很可能这个代码无法在C ++版本中尝试遵循标准。

来自cppreference : const volatile qualifiers 易失性访问会停止将工作从之前移动到之后,以及从之后到工作之前的优化。

  

&#34;这使得易失性对象适合与信号处理程序通信,但不适用于另一个执行线程&#34;

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

另见kernel.org why volatile is nearly always wrong in kernel

  

这种实现在GCC和Linux支持的所有架构上都是安全的吗? (在某些架构上,这至少是低效的,对吗?)

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

  

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

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