Interlocked.Exchange和Volatile.Write之间的区别?

jav*_*red 21 c# .net-4.5

Interlocked.Exchange和Volatile.Write有什么区别?

两种方法都更新某些变量的值.有人可以总结何时使用它们?

http://msdn.microsoft.com/ru-ru/library/bb337971http://msdn.microsoft.com/en-us/library/gg712713.aspx

特别是我需要更新我的数组的双项,我希望另一个线程看到最新的值.什么是首选?Interlocked.Exchange(ref arr[3], myValue)Volatile.Write(ref arr[3], info);在那里arr被声明为double

================================================== ==========================真实的例子,我声明了这样的双数组:

private double[] _cachedProduct;
Run Code Online (Sandbox Code Playgroud)

在一个线程中,我更新它:

        _cachedProduct[instrumentId] = calcValue;
        ...
        are.Set();
Run Code Online (Sandbox Code Playgroud)

在另一个线程中,我像这样读取这个数组:

        while(true) {
            are.WaitOne();
            ...
                result += _cachedProduct[instrumentId];
            ...
        }
Run Code Online (Sandbox Code Playgroud)

对我来说它只是工作正常.然而,为了确保"它将永远有效",无论它看起来我应该添加Volatile.WriteInterlocked.Exchange.因为双重更新不能保证是原子的http://msdn.microsoft.com/en-us/library/aa691278%28VS.71%29.aspx

在这个问题的答案中,我希望看到Volatile和Interlocked类的详细比较.为什么我们需要2节课?哪一个和何时使用?

nme*_*zes 11

Interlocked.Exchange使用保证原子操作的处理器指令.

Volatile.Write也是如此,但它还包括一个内存屏障操作.由于Windows 8上支持ARM处理器,我认为微软在DotNet 4.5上添加了Volatile.Write.英特尔和ARM处理器在内存操作重新排序方面存在差异.

在Intel上,您可以保证内存访问操作将按照发出的顺序完成,或者至少不会重新排序写操作.

从英特尔®64和IA-32架构软件开发人员手册,第8章:

8.2.2 P6及更近期处理器系列中的内存排序英特尔酷睿2双核,英特尔凌动,英特尔酷睿双核,奔腾4和P6系列处理器也使用处理器排序的内存排序模型,可进一步定义为"写入"以存储缓冲区转发命令."此模型的特征如下.

在ARM上,您没有这种保证,因此需要内存屏障.可以在此处找到解释此问题的ARM博客:http://blogs.arm.com/software-enablement/594-memory-access-ordering-part-3-memory-access-ordering-in-the-arm-architecture/

在您的示例中,由于不保证double的操作是原子操作,我建议使用锁来访问它.请记住,在读取和设置值时,必须对代码的两个部分使用锁定.

更完整的示例更好地回答您的问题,因为不清楚在设置这些值之后会发生什么.对于向量,如果您有更多的读者而不是编写者,请考虑使用ReaderWriterLockSlim对象:http://msdn.microsoft.com/en-us/library/system.threading.readerwriterlockslim.aspx

线程数和读/写频率可以显着改变锁定策略.


Pet*_*des 8

如果您不关心旧值,并且不需要完整的内存屏障(包括昂贵的 StoreLoad,即在以后加载之前耗尽存储缓冲区),请始终使用Volatile.Write.

Volatile.Write - 原子发布商店

Volatile.Write是一个具有“发布”语义的存储,AArch64 可以便宜地做到这一点,而 x86 可以免费做到这一点(嗯,与非原子存储的成本相同,当然除了与也试图写入该行的其他内核的争用)。它基本上相当于C++ std::atomic<T> store(value, memory_order_release)

例如,对于doublex86 Volatile.Write(包括 32 位和 x86-64),可以直接从 XMM 寄存器编译为 SSE2 8 字节存储,例如movsd [mem], xmm0,因为 x86 存储已经具有与MS 文档指定Volatile.Write的一样多的排序为了。假设double是自然对齐的(任何 C# 运行时都会这样做,对吧?)它也保证是原子的。(在所有 x86-64 CPU 上,以及自 P5 Pentium 以来的 32 位 CPU。)

实践中较旧的Thread.VolatileWrite方法使用完整的屏障,而不仅仅是可以在一个方向上重新排序的释放操作。这使得它并不比 Interlocked.Exchange 便宜,或者在非 x86 上也便宜不了多少。但是Volatile.Write/Read不存在某些软件可能依赖的过于强大的实现的问题。他们不必耗尽存储缓冲区,只需确保所有较早的存储(和加载)在此时都可见。


Interlocked.Exchange- 原子 RMW 加上全屏障(​​至少 acq/rel)

这是 x86 指令的包装器xchglock即使机器代码省略了前缀,它的作用就好像它有前缀一样。这意味着原子 RMW 以及作为其中一部分的“完整”屏障(如 x86mfence)。

一般来说,我认为 Interlocked 类方法起源于带有lock前缀的 x86 指令的包装器;在 x86 上,不可能执行完全屏障的原子 RMW。MS C++ 函数也具有这些名称,因此这段历史早于 C#。

MS 网站上的 Interlocked 方法(MemoryBarrier 除外)的当前文档甚至没有提到这些方法是一个完整的屏障,即使在原子 RMW 操作不需要的非 x86 ISA 上也是如此。

我不确定完整的障碍是否是实现细节而不是语言规范的一部分,但目前确实如此。这使得Intelocked.Exchange如果您不需要的话,

这个答案引用了 ECMA-335 规范,说互锁操作执行隐式获取/释放操作。 如果这像 C++ 一样acq_rel,那么这是相当强的排序,因为它是一个原子 RMW,加载和存储在某种程度上捆绑在一起,并且每个都阻止在一个方向上重新排序。(但是请参阅出于排序目的,原子读取-修改-写入是一个操作还是两个操作? - 在 C++ 语义允许的限制内,可以在 AArch64 上seq_cst通过后续relaxed操作观察 RMW 重新排序。尽管如此,它仍然是一个原子 RMW .)

@Theodor Zoulias 在网上找到了多个消息来源,称 C# 互锁方法意味着完整的栅栏/屏障。例如,Joseph Albahari 的在线书籍:“以下隐式生成完整栅栏:[...] 类上的所有方法Interlocked”。在 Stack Overflow 上,内存屏障生成器在其列表中包含所有Interlocked类方法。这两者可能只是对当前的实际行为进行分类,而不是语言规范所规定的行为。

我假设现在有大量的代码依赖于它,如果 Interlocked 方法从 C++ 变为std::memory_order_seq_cstMSrelaxed文档暗示的那样,并且对内存排序只字未提,那么它们就会中断。到周围的代码。(除非文档中其他地方对此进行了介绍。)

我自己不使用 C#,所以我无法轻松地在 SharpLab 上使用 JITted asm 编写一个示例来进行检查,但MSVC 编译_InterlockedIncrement内部函数以包含dmb ishAArch64 的内部函数。(评论线程。)因此,如果 MS 编译器对 C# 代码执行相同的操作,那么似乎 MS 编译器甚至超越了 ECMA 语言规范所保证的获取/释放,并添加了完整的屏障。

顺便说一句,有些人只使用术语“原子”来描述 RMW 操作,而不是原子加载或原子存储。MS 的文档称该类Interlocked“为多个线程共享的变量提供原子操作”。但该类不提供纯存储或纯加载,这很奇怪。

(除了 之外Read([U]Int64),大概是为了用desired=expected来公开32位x86,lock cmpxchg8b所以你要么用它自己替换一个值,要么加载旧值。无论哪种方式,它都会弄脏缓存行(因此与其他线程的读取竞争,就像任何其他互锁一样) RMW 操作)并且是一个完整的障碍,因此您通常不会在 32 位汇编中以这种方式读取 64 位整数。现代 32 位代码可以只使用 SSE2 movq xmm0, [mem]//或类似的,就像 G++ 和 MSVC 所做的那样movd eax, xmm0pextrd edx, xmm0, 1std::atomic<uint64_t>;所做的那样这要好得多,并且可以扩展到并行读取相同值的多个线程,而不会相互竞争。)

(ISO C++ 正确地做到了这一点,其中std::atomic<T>有 load 和 store 方法,以及交换、fetch_add 等。但是 ISO C++ 实际上没有定义任何关于普通非原子对象的不同步读+写或写+写所发生的情况。像 C# 这样的内存安全语言必须定义更多。)


线程间延迟

Volatile.Write 是否有可能存在一些隐藏的缺点,例如与 Interlocked.Exchange 相比“不太即时”更新内存(如果这有意义的话)?

我不期望有任何区别。 额外的内存排序只会使当前线程中的后续内容等待,直到存储提交到 L1d 缓存之后。 它不会让这种情况更快发生,因为 CPU 已经尽可能快地做到了这一点。(为了在存储缓冲区中为以后的存储腾出空间。)请参阅除了提供必要的保证之外,硬件内存屏障是否还能使原子操作的可见性更快?了解更多。

当然不是在 x86 上;我不知道在弱有序 ISA 上情况是否会有所不同,其中宽松的原子 RMW 可以加载+存储而无需等待存储缓冲区耗尽,并且可能“插队”。但 Interlocked.Exchange 并没有做轻松的 RMW,它更像是 C++ memory_order_seq_cst


问题中的示例:

在第一个示例中,使用.Set().WaitOne()单独的变量,已经提供了足够的同步,保证对 a 的普通非原子赋值double对该读者完全可见。 Volatile.Write两者Interlocked.Exchange都毫无意义。

对于释放锁,是的,您只需要一个纯粹的存储,特别是在 x86 上,它不需要任何屏障指令。如果要检测双重解锁(解锁已解锁的锁),请在存储之前先加载自旋锁变量。(与原子交换不同,这可能会错过双重解锁,但应该足以找到有缺陷的用法,除非它们总是仅在两个解锁器之间的时间紧迫时发生。)


归档时间:

查看次数:

3387 次

最近记录:

9 年,6 月 前