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

cha*_*r m 104 c# multithreading atomicity volatility

在我的多线程ASMX web服务,我有我自己的类型SystemData的类别字段_allData它由数List<T>Dictionary<T>标记为volatile.系统data(_allData)会偶尔刷新一次,我会通过创建另一个被调用的对象newData并用新数据填充它的数据结构来实现.当它完成后我就分配了

private static volatile SystemData _allData

public static bool LoadAllSystemData()
{
    SystemData newData = new SystemData();
    /* fill newData with up-to-date data*/
     ...
    _allData = newData.
} 
Run Code Online (Sandbox Code Playgroud)

这应该工作,因为赋值是原子的,并且具有对旧数据的引用的线程继续使用它,而其余的在分配之后具有新的系统数据.然而,我的同事说,volatile我应该使用,而不是使用关键字和简单的InterLocked.Exchange分配,因为他说在某些平台上,不能保证引用赋值是原子的.另外:当我宣布the _allData字段volatile

Interlocked.Exchange<SystemData>(ref _allData, newData); 
Run Code Online (Sandbox Code Playgroud)

产生警告"对易变场的引用不会被视为不稳定的"我应该怎么看待这个?

Eri*_*ert 173

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

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

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

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

在所有.NET平台上,参考分配都保证是原子的.

我的同事正在虚假处理.这是否意味着他们的结论不正确?

不必要.你的同事可能会因为不好的理由给你很好的建议.也许还有一些其他原因你应该使用Interlocked.Exchange.无锁编程非常困难,当您离开该领域专家所支持的成熟实践的那一刻,您就会处于杂草之中并冒着最恶劣的竞争条件.我不是这个领域的专家,也不是你的代码专家,所以我不能以某种方式作出判断.

产生警告"对易变场的引用不会被视为不稳定的"我应该怎么看待这个?

你应该理解为什么这是一般的问题.这将导致理解为什么警告在这种特殊情况下不重要.

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

(我假设你已经理解了所有这些.如果你没有详细了解volatile的含义以及它如何影响处理器缓存语义,那么你就不明白它是如何工作的,不应该使用volatile.无锁程序很难做到正确;确保你的程序是正确的,因为你了解它是如何工作的,而不是偶然的.)

现在假设您通过将ref传递给该字段来创建一个变量,该变量是volatile字段的别名.在被调用的方法中,编译器没有任何理由知道引用需要具有volatile语义!编译器将为无法实现volatile字段规则的方法快速生成代码,但变量 volatile字段.这完全破坏了你的无锁逻辑; 假设始终是挥发性场挥发性语义访问.有时将其视为易变性是没有意义的,而不是其他时间; 你必须始终保持一致,否则你无法保证其他访问的一致性.

因此,编译器会在您执行此操作时发出警告,因为它可能会完全破坏您精心开发的无锁逻辑.

当然,Interlocked.Exchange 编写为期望一个易变的字段并做正确的事情.因此该警告具有误导性.我非常后悔; 我们应该做的是实现一些机制,其中像Interlocked.Exchange这样的方法的作者可以在方法上放置一个属性,说"这个方法采用ref对变量强制执行volatile语义,因此禁止警告".也许在未来版本的编译器中我们将这样做.

  • @Mike:谈到在低锁多线程情况下可能观察到的情况,我和下一个人一样无知.答案可能因处理器而异.您应该向专家提出您的问题,或者如果您感兴趣,请阅读该主题.Joe Duffy的书和他的博客是开始的好地方.我的规则:不要使用多线程.如果必须,请使用不可变数据结构.如果你不能,请使用锁.只有当你*必须*没有锁的可变数据时才应考虑低锁技术. (12认同)
  • 据我所知,Interlocked.Exchange 还保证会创建内存屏障。因此,例如,如果您创建一个新对象,然后分配几个属性,然后将该对象存储在另一个引用中而不使用 Interlocked.Exchange,那么编译器可能会弄乱这些操作的顺序,从而使访问第二个引用而不是线程 -安全的。真的是这样吗?使用 Interlocked.Exchange 是那种场景吗? (2认同)
  • @EricLippert在"不使用多线程"和"如果必须使用不可变数据结构"之间,我会插入中间和非常常见的级别"让子线程只使用独占的输入对象而父线程使用结果只有当孩子完成时".与`var myresult = await Task.Factory.CreateNew(()=> MyWork(exclusiveLocalStuffOrValueTypeOrCopy));`一样. (2认同)

Guf*_*ffa 9

你的同事是错的,或者他知道C#语言规范没有的东西.

5.5变量引用的原子性:

"以下数据类型的读写是原子的:bool,char,byte,sbyte,short,ushort,uint,int,float和reference types."

因此,您可以写入易失性参考,而不会有获得损坏值的风险.

您当然应该小心决定哪个线程应该获取新数据,以最大限度地降低一次多个线程执行此操作的风险.

  • @guffa:是的我也读过.这留下了原始问题"引用赋值是原子的,为什么需要Interlocked.Exchange(ref Object,Object)?" unaswered (3认同)

sel*_*rer 7

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

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

进入或离开临界区的函数

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

等待功能

联锁功能

同步和多处理器问题

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

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


Gui*_*ume 6

Interlocked.Exchange <T>

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

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

除非分析器被证明是您应用程序中的瓶颈,否则您应该考虑使用unsing锁,更容易理解并证明您的代码是正确的.