【问题标题】:Do spin locks always require a memory barrier? Is spinning on a memory barrier expensive?自旋锁总是需要内存屏障吗?在内存屏障上旋转是否昂贵?
【发布时间】:2011-10-12 05:59:06
【问题描述】:

我写了一些可以在本地正常工作的无锁代码 读取,在大多数情况下。

内存读取上的本地旋转是否一定意味着我 必须始终在旋转之前插入内存屏障 读了吗?

(为了验证这一点,我设法制作了一个阅读器/编写器 导致读者永远看不到的组合 书面价值,在某些非常具体的情况下 条件——专用 CPU,附加到 CPU 的进程, 优化器一直向上,没有其他工作完成 循环——所以箭头确实指向那个方向,但我不是 完全确定通过内存旋转的成本 障碍。)

如果是,旋转穿过内存屏障的成本是多少 缓存的存储缓冲区中没有要刷新的内容? 即,所有过程正在做(在 C 中)是

while ( 1 ) {
    __sync_synchronize();
    v = value;
    if ( v != 0 ) {
        ... something ...
    }
}

假设它是免费的并且不会妨碍我是否正确 内存总线有流量吗?

另一种说法是问:内存屏障是否有用 除了:刷新存储缓冲区,应用 对其无效,并阻止编译器 重新排序其位置的读/写?


反汇编,__sync_synchronize() 似乎翻译成:

lock orl

来自英特尔手册(对于新手来说同样含糊不清):

Volume 3A: System Programming Guide, Part 1 --   8.1.2

Bus Locking

Intel 64 and IA-32 processors provide a LOCK# signal that
is asserted automatically during certain critical memory
operations to lock the system bus or equivalent link.
While this output signal is asserted, requests from other
processors or bus agents for control of the bus are
blocked.

[...]

For the P6 and more recent processor families, if the
memory area being accessed is cached internally in the
processor, the LOCK# signal is generally not asserted;
instead, locking is only applied to the processor’s caches
(see Section 8.1.4, “Effects of a LOCK Operation on
Internal Processor Caches”).

我的翻译:“当你说 LOCK,这会很贵,但我们 只在必要时才这样做。”


@BlankXavier:

我确实测试过,如果写入器没有明确地将写入从存储缓冲区中推出,并且它是该 CPU 上运行的唯一进程,则读取器可能永远看到写入器的效果(我可以用一个测试程序重现它,但正如我上面提到的,它只发生在一个特定的测试中,具有特定的编译选项和专用的核心分配——我的算法工作正常,只有当我对它的工作原理感到好奇并写了我意识到它可能会在未来出现问题的显式测试)。

我认为默认情况下简单写入是 WB 写入(回写),这意味着它们不会立即被刷新,但读取将采用它们的最新值(我认为他们称之为“存储转发”)。所以我为作者使用了 CAS 指令。我在 Intel 手册中发现了所有这些不同类型的写入实现(UC、WC、WT、WB、WP),Intel vol 3A 第 11-10 章,仍在学习它们。

我的不确定性在于读者方面:我从 McKenney 的论文中了解到还有一个失效队列,一个从总线到缓存的传入失效队列。我不确定这部分是如何工作的。特别是,您似乎暗示循环通过正常读取(即,非锁定,没有障碍,并且仅使用 volatile 以确保优化器在编译后离开读取)每次都会检查“无效队列” (如果存在这样的事情)。如果一个简单的读取不够好(即可以读取一个旧的缓存行,它仍然显示为有效等待队列失效(这对我来说听起来也有点不连贯,但是失效队列如何工作呢?)),那么原子读取将是必要的,我的问题是:在这种情况下,这会对公共汽车有什么影响吗? (我想可能不会。)

我仍在阅读英特尔手册,虽然我看到了关于存储转发的精彩讨论,但我还没有找到关于失效队列的很好讨论。我决定将我的 C 代码转换为 ASM 并进行实验,我认为这是真正了解其工作原理的最佳方式。

【问题讨论】:

  • "在大多数情况下都可以很好地用于本地读取。" - 如果它不能总是“正常”工作,那就不好......
  • 关于完全优化的小循环测试,还有其他问题,例如Cyrix coma bug(即使在这种情况下不适用),这可能会影响“假”测试。
  • @Mitch:当然,这就是我问的原因:-)
  • “本地旋转”是什么意思?这与“远程旋转”有何不同?什么是远程旋转?
  • @Blank:我使用的是 Herlihy/Shavit 书中的术语:(p.147)“这种“本地旋转”的概念,其中线程重复地重新读取缓存值而不是重复使用总线,是设计高效自旋锁的重要原则。”

标签: lock-free spinlock memory-barriers barrier


【解决方案1】:

“xchg reg,[mem]”指令将通过内核的 LOCK 引脚发出其锁定意图。该信号穿过其他内核并缓存到总线主控总线(PCI变体等),总线主控总线将完成它们正在做的事情,最终LOCKA(确认)引脚将向CPU发出xchg可能完成的信号。然后 LOCK 信号关闭。此序列可能需要很长时间(数百个 CPU 周期或更多)才能完成。之后,其他内核的相应缓存行将失效,您将拥有一个已知状态,即已在内核之间同步的状态。

xchg 指令是实现原子锁所必需的。如果锁本身是成功的,您就可以访问您已定义锁以控制访问的资源。这样的资源可以是内存区域、文件、设备、功能或您拥有的任何东西。尽管如此,程序员始终可以编写代码在该资源被锁定时使用它,而在它没有被锁定时不使用它。通常情况下,成功锁定后的代码序列应尽可能短,以便尽可能少地阻碍其他代码获取对资源的访问权。

请记住,如果锁定不成功,您需要通过发出新的 xchg 重试。

“无锁”是一个吸引人的概念,但它需要消除共享资源。如果您的应用程序有两个或多个内核同时读取和写入公共内存地址,则“无锁”不是一种选择。

【讨论】:

    【解决方案2】:

    我可能没有正确理解这个问题,但是......

    如果你在旋转,一个问题是编译器优化你的旋转。 Volatile 解决了这个问题。

    内存屏障,如果你有的话,将由 writer 发布给自旋锁,而不是 reader。作者实际上必须使用一个 - 这样做可以确保立即将写入推送出去,但无论如何它很快就会退出。

    屏障防止执行该代码的线程跨其位置重新排序,这是它的另一个成本。

    【讨论】:

      【解决方案3】:

      请记住,屏障通常用于对内存访问集进行排序,因此您的代码很可能在其他地方也需要屏障。例如,屏障要求看起来像这样并不少见:

      while ( 1 ) {
      
          v = pShared->value;
          __acquire_barrier() ;
      
          if ( v != 0 ) {
              foo( pShared->something ) ;
          }
      }
      

      此屏障将阻止 if 块(即:pShared->something)中的加载和存储在 value 加载完成之前执行。一个典型的例子是,您有一些“生产者”使用 v != 0 的存储来标记其他一些内存 (pShared->something) 处于其他预期状态,如:

      pShared->something = 1 ;  // was 0
      __release_barrier() ;
      pShared->value = 1 ;  // was 0
      

      在这个典型的生产者消费者场景中,您几乎总是需要成对的屏障,一个用于标记辅助内存可见的存储(以便在某物存储之前看不到值存储的效果),以及消费者的一个障碍(以便在值加载完成之前不会开始加载某项内容)。

      这些障碍也是特定于平台的。例如,在 powerpc 上(使用 xlC 编译器),您将分别使用 __isync()__lwsync() 作为消费者和生产者。所需的障碍还可能取决于您用于存储和加载value 的机制。如果您使用了导致 intel LOCK(可能是隐式)的原子内在函数,那么这将引入一个隐式屏障,因此您可能不需要任何东西。此外,您可能还需要明智地使用 volatile(或者最好使用在幕后这样做的原子实现)以使编译器执行您想要的操作。

      【讨论】:

        猜你喜欢
        • 2021-12-20
        • 1970-01-01
        • 2017-01-05
        • 2012-04-07
        • 2012-09-08
        • 2018-10-02
        • 1970-01-01
        • 2014-01-20
        • 1970-01-01
        相关资源
        最近更新 更多