旋转锁总是需要内存屏障吗?在记忆障碍上旋转昂贵吗?

时间:2011-07-25 00:31:27

标签: lock-free spinlock memory-barriers barrier

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

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

(为了证实这一点,我成功地制作了一个读者/作家 组合导致读者永远不会看到 书面价值,在某些非常具体的 条件 - 专用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指令。我在英特尔手册中发现了所有这些不同类型的写实现(UC,WC,WT,WB,WP),英特尔第3卷第11-10章,仍在了解它们。

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

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

3 个答案:

答案 0 :(得分:4)

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

xchg指令是实现原子锁所必需的。如果锁本身成功,则您可以访问已定义锁以控制访问权限的资源。这样的资源可以是存储区域,文件,设备,功能或者你有什么。尽管如此,程序员始终要编写在锁定时使用此资源的代码,而在没有锁定时则编写代码。通常,成功锁定后的代码序列应尽可能短,以便尽可能少地阻止其他代码获取对资源的访问。

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

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

答案 1 :(得分:2)

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

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

内存屏障(如果有的话)将由 writer 发布到自旋锁,而不是读取器。作者实际上没有使用一个 - 这样做可以确保写入立即被推出,但它很快就会很快就会出来。

屏障阻止执行该代码的线程在其位置重新排序,这是其他成本。

答案 2 :(得分:0)

请记住,障碍通常用于订购内存访问集,因此您的代码很可能在其他地方也需要障碍。例如,屏障要求看起来并不常见:

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(或者最好使用在封面下执行此操作的原子实现),以便让编译器执行您想要的操作。