Interlocked.CompareExchange使用内存屏障吗?

26

我正在阅读Joe Duffy关于Volatile reads and writes, and timeliness的帖子,并试图理解帖子中最后一个代码示例的某些内容:

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不使用内存屏障,则整个锁获取过程将不公平,因为很可能第一个获取锁的线程将是获取后续所有锁的线程。我理解正确吗,还是我漏掉了什么? 编辑:实际上,主要问题是在调用CompareExchange之前是否会导致内存屏障读取m_state的值。 所以当它们再次尝试调用CompareExchange时,分配0是否可见于所有线程。
6个回答

27
任何带有lock前缀的x86指令都具有完全的内存屏障。如Abel的答案所示,Interlocked * API和CompareExchanges使用了带有lock前缀的指令,例如lock cmpxchg。因此,它意味着内存屏障。
是的,Interlocked.CompareExchange使用了内存屏障。
为什么?因为x86处理器这样做。从Intel的Volume 3A:系统编程指南第1部分,第7.1.2.2节中可以看到:
对于P6系列处理器,锁操作序列化所有未完成的加载和存储操作(等待它们完成)。对于Pentium 4和Intel Xeon处理器也是如此,有一个例外。引用弱排序内存类型(如WC内存类型)的加载操作可能不会被序列化。 volatile与此讨论无关。这是关于原子操作的;为了支持CPU中的原子操作,x86保证所有先前的加载和存储操作都已经完成。

值得一提的是,它提供完整的栅栏而不是半栅栏。 - Royi Namir
1
Interlocked.CompareExchange 在 ARM / AArch64 上也是如此吗?还是这只是针对 x86 的 C# 实现细节,不属于语言标准保证的一部分? - Peter Cordes
@PeterCordes,看起来ARM不同,参见https://devblogs.microsoft.com/oldnewthing/20130913-00/?p=3243——使用了两个特定的原子指令来进行加载链接/条件策略。 - Kind Contributor
2
@KindContributor:在ARMv8.1之前,没有单指令原子比较交换。但是ARM/AArch64也有完整的内存屏障指令,因此可能需要使用dmb ish。微软的C++ _InterlockedIncrement intrinsic文档表明有_acq_rel版本,推测普通版本为seq_cst。(因为没有_relaxed或_sc)。 - Peter Cordes
1
在ARM上进行seq_cst RMW(使用ldaxr / stlxr操作可能与完整的屏障不完全相同,但我认为它仍将阻止两侧操作彼此重新排序,即使自己的load / store 可能会出现分裂。这比仅使用ldxr / stxr(具有独占但没有获取和释放属性,例如C ++ memory_order_relaxed)的完全“松散”CAS要接近得多。 - Peter Cordes
1
我在C++中进行了测试https://godbolt.org/z/c95shc9Yc,并且MSVC同时使用`ldaxr/stlxr`和`dmb ish(数据内存屏障:内部共享)用于C++ _InterlockedIncrement`。因此,至少该实现使其成为一个真正的全障碍,超越了序列一致操作,我猜测C#也可能如此。只剩下任何书面规范是否要求这样的问题。 - Peter Cordes

10

ref 在像是下面的情况中无法遵守通常的 volatile 规则:

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

在这里,即使底层字段(myField)是volatile的,RunMethod也不能保证检测到isDone的外部更改;RunMethod不知道它,因此没有正确的代码。

然而!这应该不是问题:

  • 如果您使用Interlocked,则对字段的所有访问都使用Interlocked
  • 如果您使用lock,则对字段的所有访问都使用lock

按照这些规则操作,应该可以正常工作。


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


我不是在询问关于易失性的问题,而只是想知道在释放锁时是否需要Interlocked.Exchange(或者Thread.VolatileWrite更合适)。 此代码可能出现的唯一问题是“不公平”的习惯(正如Joe在帖子开头提到的那样)。 - unknown
@Marc:InternalCall方法的源代码(大部分)可以通过共享源代码CLI SSCLI,又称Rotor进行查看。Interlocked.CompareExchange在这篇有趣的文章中有解释:http://www.moserware.com/2008/09/how-do-locks-lock.html - Abel

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中看到。


是的,它必须在x86上运行,但在ARM或AArch64上是否也是完整的屏障,其中硬件可以执行弱有序原子RMW? - Peter Cordes
@PeterCordes,这个问题我在2009年就回答过了。当时还没有ARM版本的.NET,只有x86/x64(也许还有PowerPC)。但是现在.NET已经全部开源,检查Mono和RyuJIT都非常容易。 - Abel
1
我在尝试检查时发现了这个答案;这是谷歌上出现的东西之一。我正在寻找有文档保证的内容,因此检查源代码并不是很好。(我几乎不会说“微不足道”。可能很简单,但可能需要花费时间。)这个答案对于2009年来说还可以,我的观点是它对当前读者的用处不如它本应该有的那么大。结果发现这个问题的另一个答案引用了一个标准,证明Interlocked操作至少是Acquire / Release。 - Peter Cordes
1
@PeterCordes,你的评论引起了我的兴趣。我已经检查了实际实现和Github讨论,看起来这取决于版本。我已经更新了我的答案以包括这一点,如果你在这个主题上找到更多信息,请随意进一步编辑我的答案。 - Abel

4

MSDN关于Win32 API函数的介绍如下:

"大多数交错函数在所有Windows平台上都提供完整的内存屏障"

(例外情况是具有显式Acquire / Release语义的交错函数)

由此可见,C#运行时的Interlocked函数提供了相同的保证,因为它们记录了相同的行为(并且在我所知道的平台上解析为内置CPU语句)。不幸的是,由于MSDN倾向于发布示例而非文档,这一点没有明确说明。


2
根据ECMA-335(第I.12.6.5节)的规定:
5. 显式原子操作。类库在System.Threading.Interlocked类中提供了各种原子操作。这些操作(例如,Increment、Decrement、Exchange和CompareExchange)执行隐式获取/释放操作。因此,这些操作遵循“最小惊讶原则”。

2
交错函数保证在解决操作数时会阻塞总线和cpu。直接的结果是,无论是你的cpu还是其他cpu上的线程切换,在交错函数执行期间都不会中断它。
由于你正在向c#函数传递引用,底层汇编代码将使用实际整数的地址,因此变量访问不会被优化掉。它将按预期正常工作。
编辑:这里有一个链接,更好地解释了asm指令的行为:http://faydoc.tripod.com/cpu/cmpxchg.htm。可以看到,强制写入周期通过阻塞总线,因此任何其他“线程”(即其他cpu核心)尝试同时使用总线的线程将被放置在等待队列中。

实际上,相反的情况(部分地)是正确的。Interlocked执行原子操作并使用cmpxchg汇编指令。它不需要将其他线程置于等待状态,因此非常高效。请参阅此页面上的“Inside InternalCall”部分:http://www.moserware.com/2008/09/how-do-locks-lock.html - Abel
现代CPU中没有共享总线;在对齐值上的“lock cmpxchg”只能让该CPU核心延迟响应MESI失效/共享请求,即缓存锁而不是总线锁。无论如何,这只告诉我们关于x86,而不是其他ISA的C#的一般情况。 - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接