引用分配是原子的,那么为啥需要 Interlocked.Exchange(ref Object, Object)?

Posted

技术标签:

【中文标题】引用分配是原子的,那么为啥需要 Interlocked.Exchange(ref Object, Object)?【英文标题】:reference assignment is atomic so why is Interlocked.Exchange(ref Object, Object) needed?引用分配是原子的,那么为什么需要 Interlocked.Exchange(ref Object, Object)? 【发布时间】:2011-01-12 14:57:20 【问题描述】:

在我的多线程 asmx Web 服务中,我有一个我自己的 SystemData 类型的类字段 _allData,它由几个 List<T>Dictionary<T> 组成,标记为 volatile。系统数据 (_allData) 会不时刷新一次,我通过创建另一个名为 newData 的对象来做到这一点,并用新数据填充它的数据结构。完成后,我只需分配

private static volatile SystemData _allData

public static bool LoadAllSystemData()

    SystemData newData = new SystemData();
    /* fill newData with up-to-date data*/
     ...
    _allData = newData.
 

这应该可以工作,因为分配是原子的,并且引用旧数据的线程继续使用它,其余的线程在分配后就拥有新的系统数据。但是我的同事说我应该使用InterLocked.Exchange 而不是使用volatile 关键字和简单的分配,因为他说在某些平台上不能保证引用分配是原子的。此外:当我将the _allData 字段声明为volatile 时,

Interlocked.Exchange<SystemData>(ref _allData, newData); 

产生警告“对 volatile 字段的引用不会被视为 volatile”我应该怎么想?

【问题讨论】:

【参考方案1】:

要么你的同事弄错了,要么他知道 C# 语言规范不知道的东西。

Atomicity of variable references:

"读取和写入以下内容 数据类型是原子的:bool、char、 字节,sbyte,短,ushort,uint,int, 浮点数和引用类型。”

因此,您可以写入 volatile 引用而不会有损坏值的风险。

您当然应该小心决定哪个线程应该获取新数据,以尽量减少一次多个线程执行此操作的风险。

【讨论】:

@guffa:是的,我也读过。这留下了最初的问题“引用分配是原子的,那么为什么需要 Interlocked.Exchange(ref Object, Object)?”没有回答 @zebrabox:你是什么意思?当他们不是?你会怎么做? @matti:当您必须以原子操作的形式读取和写入值时需要它。 您多久需要担心内存在 .NET 中没有正确对齐?互操作繁重的东西? @zebrabox:规范没有列出该警告,它给出了非常明确的声明。您是否有针对非内存对齐情况的参考,其中引用读取或写入无法原子化?似乎这会违反规范中非常明确的语言。【参考方案2】:

Iterlocked.Exchange() 不仅是原子的,它还负责内存可见性:

以下同步函数使用适当的屏障来确保内存排序:

进入或离开临界区的函数

向同步对象发送信号的函数

等待函数

联锁功能

Synchronization and Multiprocessor Issues

这意味着除了原子性之外,它还确保:

对于调用它的线程: 没有对指令进行重新排序(由编译器、运行时或硬件)。 对于所有线程: 在该指令之前没有从内存读取将看到在该指令之后发生的内存更改(由调用该指令的线程)。这听起来很明显,但缓存行可能不是按照写入顺序刷新到主内存。 该指令之后的所有读取都将看到该指令所做的更改以及该指令之前所做的所有更改(由调用该指令的线程)。 在此指令更改到达主内存后,将在此指令更改到达主内存后发生所有对内存的写入(通过在完成后将此指令更改刷新到主内存,而不是让硬件刷新它自己的时间)。

【讨论】:

【参考方案3】:

这里有很多问题。一次考虑一个:

引用分配是原子的,为什么需要 Interlocked.Exchange(ref Object, Object)?

引用分配是原子的。 Interlocked.Exchange 不仅仅进行引用分配。它读取变量的当前值,隐藏旧值,并将新值分配给变量,所有这些都是原子操作。

我的同事说,在某些平台上,不能保证引用分配是原子的。我的同事说的对吗?

没有。保证引用分配在所有 .NET 平台上都是原子的。

我的同事是根据错误的前提进行推理的。这是否意味着他们的结论不正确?

不一定。你的同事可能出于不好的原因给了你很好的建议。也许您应该使用 Interlocked.Exchange 还有其他一些原因。无锁编程非常困难,一旦您偏离了该领域专家支持的成熟实践,您就会陷入困境并冒着最糟糕的竞争条件的风险。我既不是这个领域的专家,也不是你的代码方面的专家,所以我无法做出这样或那样的判断。

产生警告“对 volatile 字段的引用不会被视为 volatile”我应该怎么想?

您应该了解为什么这是一个普遍的问题。这将有助于理解为什么警告在这种特殊情况下并不重要。

编译器发出此警告的原因是因为将字段标记为 volatile 意味着“该字段将在多个线程上更新——不要生成任何缓存该字段值的代码,并确保任何读取或该字段的写入不会通过处理器缓存不一致“及时向前和向后移动”。

(我假设您已经了解所有这些。如果您对 volatile 的含义以及它如何影响处理器缓存语义没有详细的了解,那么您将不了解它的工作原理并且不应该使用 volatile。锁- 免费程序很难做到正确;请确保您的程序正确,因为您了解它的工作原理,而不是偶然正确。)

现在假设您通过将 ref 传递给该字段来创建一个变量,该变量是 volatile 字段的别名。在被调用的方法内部,编译器没有任何理由知道引用需要具有可变语义!编译器会很乐意为未能实现 volatile 字段规则的方法生成代码,但变量 一个 volatile 字段。这会彻底破坏你的无锁逻辑;假设始终始终使用 volatile 语义访问 volatile 字段。有时将其视为易失性而不是其他时候是没有意义的;您必须始终保持一致,否则您无法保证其他访问的一致性。

因此,当您执行此操作时,编译器会发出警告,因为它可能会完全打乱您精心开发的无锁逻辑。

当然,Interlocked.Exchange 是为了期待一个易变的字段并做正确的事情而编写的。因此,该警告具有误导性。我对此感到非常遗憾;我们应该做的是实现某种机制,这样 Interlocked.Exchange 这样的方法的作者可以在方法上放置一个属性,说“这个采用 ref 的方法对变量强制执行 volatile 语义,因此抑制警告”。也许在编译器的未来版本中我们会这样做。

【讨论】:

据我所知,Interlocked.Exchange 还保证创建了内存屏障。因此,例如,如果您创建一个新对象,然后分配几个属性,然后将对象存储在另一个引用中而不使用 Interlocked.Exchange,那么编译器可能会弄乱这些操作的顺序,从而使访问第二个引用不是线程-安全的。真的是这样吗?使用Interlocked是否有意义。交换是那种场景? @Mike:当谈到在低锁多线程情况下可能观察到的情况时,我和旁边的人一样无知。答案可能因处理器而异。您应该向专家提出您的问题,或者如果您感兴趣,请阅读该主题。 Joe Duffy 的书和​​他的博客是很好的起点。我的规则:不要使用多线程。如果必须,请使用不可变数据结构。如果不能,请使用锁。只有当您必须拥有没有锁的可变数据时,您才应该考虑低锁技术。 感谢您的回答埃里克。它确实让我感兴趣,这就是为什么我一直在阅读有关多线程和锁定策略的书籍和博客,并尝试在我的代码中实现这些。但是还有很多东西要学... @EricLippert 在“不要使用多线程”和“如果必须,使用不可变数据结构”之间,我会插入中间和非常常见的级别“让子线程仅使用独占的输入对象和父线程仅在子进程完成时才使用结果”。如var myresult = await Task.Factory.CreateNew(() =&gt; MyWork(exclusivelyLocalStuffOrValueTypeOrCopy)); @John:这是个好主意。我尝试将线程视为廉价进程:它们在那里完成工作并产生结果,而不是作为主程序数据结构内的第二个控制线程运行。但是,如果线程正在做的工作量如此之大,以至于将其视为一个进程是合理的,那么我说让它成为一个进程!【参考方案4】:

Interlocked.Exchange< T >

将指定类型 T 的变量设置为指定值并返回原始值,作为原子操作。

它改变并返回原始值,它没用,因为你只想改变它,正如 Guffa 所说,它已经是原子的。

除非分析器证明它是您应用程序中的瓶颈,否则您应该考虑取消锁定,这样更容易理解并证明您的代码是正确的。

【讨论】:

以上是关于引用分配是原子的,那么为啥需要 Interlocked.Exchange(ref Object, Object)?的主要内容,如果未能解决你的问题,请参考以下文章

如果读写是原子的,为啥需要 volatile [重复]

为啥我需要将默认引用参数定义为 const 以便我可以为其分配左值? [复制]

如何区分interlock和jersey

为啥 C++ 不允许重新绑定引用?

为啥这个对象在分配其他东西时不通过引用传递?

C# bool 是原子的,为啥 volatile 有效