26

我正在阅读 Joe Duffy 的关于Volatile 读写和及时性的帖子,并且我试图了解有关帖子中最后一个代码示例的一些信息:

while (Interlocked.CompareExchange(ref m_state, 1, 0) != 0) ;
m_state = 0;
while (Interlocked.CompareExchange(ref m_state, 1, 0) != 0) ;
m_state = 0;
… 

当执行第二个 CMPXCHG 操作时,它是否使用内存屏障来确保 m_state 的值确实是写入它的最新值?还是只使用已经存储在处理器缓存中的一些值?(假设m_state未声明为 volatile)。
如果我理解正确,如果 CMPXCHG 不使用内存屏障,那么整个锁获取过程将不公平,因为第一个获取锁的线程很可能是获取所有锁的线程以下锁。我是否理解正确,或者我在这里错过了什么?

编辑:主要问题实际上是在尝试读取 m_state 的值之前调用 CompareExchange 是否会导致内存障碍。因此,当他们再次尝试调用 CompareExchange 时,是否所有线程都可以看到分配 0。

4

6 回答 6

27

任何具有锁定前缀的 x86 指令都具有完整的内存屏障。如 Abel 的回答所示,Interlocked* API 和 CompareExchanges 使用lock前缀指令,例如lock cmpxchg. 因此,它意味着内存围栏。

是的,Interlocked.CompareExchange 使用内存屏障。

为什么?因为 x86 处理器这样做了。来自英特尔的第 3A 卷:系统编程指南第 1 部分,第 7.1.2.2 节:

对于 P6 系列处理器,锁定操作会序列化所有未完成的加载和存储操作(即等待它们完成)。此规则也适用于 Pentium 4 和 Intel Xeon 处理器,但有一个例外。引用弱排序内存类型(如 WC 内存类型)的加载操作可能不会被序列化。

volatile has nothing to do with this discussion. This is about atomic operations; to support atomic operations in CPU, x86 guarantees all previous loads and stores to be completed.

于 2009-11-11T16:57:40.583 回答
10

ref不遵守通常的volatile规则,尤其是在以下方面:

volatile bool myField;
...
RunMethod(ref myField);
...
void RunMethod(ref bool isDone) {
    while(!isDone) {} // silly example
}

在这里,即使基础字段 ( ) 是;RunMethod也不能保证发现外部变化。不知道,所以没有正确的代码。isDonemyFieldvolatileRunMethod

然而!这应该不是问题:

  • 如果您正在使用Interlocked,则Interlocked用于对该字段的所有访问
  • 如果您正在使用lock,则lock用于对该字段的所有访问

遵循这些规则,它应该可以正常工作。


重新编辑;是的,这种行为Interlocked. 老实说,我不知道它是如何实现的(内存屏障等 - 请注意它们是“InternalCall”方法,所以我无法检查;-p) - 但是是的:来自一个线程的更新将立即可见所有其他人,只要他们使用这些Interlocked方法(因此我在上面的观点)。

于 2009-10-17T08:23:37.960 回答
7

似乎与同名的 Win32 API 函数有一些比较,但这个线程都是关于 C#Interlocked类的。从它的描述来看,可以保证它的操作是原子的。我不确定这如何转化为此处其他答案中提到的“完全记忆障碍”,但请自行判断。

在单处理器系统上,没有什么特别的事情发生,只有一条指令:

FASTCALL_FUNC CompareExchangeUP,12
        _ASSERT_ALIGNED_4_X86 ecx
        mov     eax, [esp+4]    ; Comparand
        cmpxchg [ecx], edx
        retn    4               ; result in EAX
FASTCALL_ENDFUNC CompareExchangeUP

但在多处理器系统上,使用硬件锁来防止其他内核同时访问数据:

FASTCALL_FUNC CompareExchangeMP,12
        _ASSERT_ALIGNED_4_X86 ecx
        mov     eax, [esp+4]    ; Comparand
  lock  cmpxchg [ecx], edx
        retn    4               ; result in EAX
FASTCALL_ENDFUNC CompareExchangeMP

在这里和那里一些错误的结论是一个有趣的阅读,但总而言之,在该主题上非常出色的是CompareExchange 上的这篇博客文章

ARM 更新

通常,答案是“视情况而定”。看来,在 2.1 之前,ARM有一个半壁垒。对于 2.1 版本,此行为已更改为操作的完全障碍Interlocked

当前代码可以在这里找到,而CompareExchange 的实际实现可以在这里找到。关于生成的 ARM 程序集的讨论以及生成代码的示例可以在上述 PR 中看到。

于 2009-11-10T00:18:35.153 回答
4

MSDN谈到 Win32 API 函数时说:“大多数互锁函数在所有 Windows 平台上都提供完整的内存屏障

(例外是具有显式获取/释放语义的互锁函数)

从中我会得出结论,C# 运行时的 Interlocked 做出了相同的保证,因为它们被记录为具有其他相同的行为(并且它们解析为我所知道的平台上的内在 CPU 语句)。不幸的是,由于 MSDN 倾向于提供示例而不是文档,因此没有明确说明。

于 2009-10-17T16:33:03.143 回答
2

互锁功能保证在解析操作数时停止总线和 CPU。直接的后果是,在您的 cpu 或另一个 cpu 上,没有线程切换会在执行过程中中断联锁功能。

由于您传递的是对 c# 函数的引用,因此底层汇编代码将使用实际整数的地址,因此变量访问不会被优化掉。它将完全按预期工作。

编辑:这里有一个链接可以更好地解释 asm 指令的行为:http: //faydoc.tripod.com/cpu/cmpxchg.htm
如您所见,总线通过强制写入周期而停止,因此任何其他“线程"(阅读:其他 cpu 内核)将尝试同时使用总线将被放入等待队列中。

于 2009-10-17T09:15:44.883 回答
2

According to ECMA-335 (section I.12.6.5):

5. Explicit atomic operations. The class library provides a variety of atomic operations in the System.Threading.Interlocked class. These operations (e.g., Increment, Decrement, Exchange, and CompareExchange) perform implicit acquire/release operations.

So, these operations follow principle of least astonishment.

于 2017-07-27T09:37:15.917 回答