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


108

在我的多线程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.
} 

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

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

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

Answers:


180

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

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

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

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

不能。在所有.NET平台上,引用分配都是原子的。

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

不必要。您的同事出于不好的原因可能会给您很好的建议。也许还有其他原因使您应该使用Interlocked.Exchange。无锁编程非常困难,一旦您偏离了该领域专家公认的成熟做法,您就会陷入困境,并冒着最恶劣的比赛条件的危险。我既不是该领域的专家,也不是您的代码的专家,所以我无法做出一种判断。

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

您应该了解为什么这通常是一个问题。这将导致理解为什么在此特定情况下警告不重要。

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

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

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

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

当然,编写Interlocked.Exchange的目的希望字段可变并做正确的事情。因此,该警告具有误导性。我对此感到非常遗憾。我们应该做的是实现一种机制,通过这种机制,像Interlocked.Exchange这样的方法的作者可以在该方法上添加一个属性,说“该方法使用ref会对变量施加易变义的语义,因此禁止了警告”。也许在将来的编译器版本中,我们会这样做。


1
据我所知,Interlocked.Exchange还保证会创建内存屏障。因此,例如,如果您创建一个新对象,然后分配几个属性,然后在不使用Interlocked.Exchange的情况下将该对象存储在另一个引用中,则编译器可能会弄乱这些操作的顺序,从而使访问第二个引用不是线程-安全。真的是这样吗?使用Interlocked.Exchange是这种情况吗?
迈克(Mike)2010年

12
@Mike:谈到低锁多线程情况下可能观察到的情况,我和下一个家伙一样无知。答案可能会因处理器而异。您应该向专家咨询您的问题,或者,如果您感兴趣,请继续阅读该主题。乔·达菲(Joe Duffy)的书和博客是开始的好地方。我的规则:不要使用多线程。如果必须,请使用不可变的数据结构。如果不能,请使用锁。仅当必须具有不带锁的可变数据时,应考虑使用低锁技术。
埃里克·利珀特

感谢您的回答Eric。确实让我很感兴趣,这就是为什么我一直在阅读有关多线程和锁定策略的书籍和博客,并尝试在代码中实现这些的原因。但是还有很多东西要学习……
Mike

2
@EricLippert在“不要使用多线程”和“如果必须使用不可变的数据结构”之间,我将插入一个中间且非常常见的级别:“让子线程仅使用独有的输入对象,而父线程则使用结果只有在孩子完蛋后”。就像在var myresult = await Task.Factory.CreateNew(() => MyWork(exclusivelyLocalStuffOrValueTypeOrCopy));
John

1
@John:那是个好主意。我试图将线程像廉价进程一样对待:它们在那里工作并产生结果,而不是在主程序的数据结构中成为第二控制线程。但是,如果线程正在做的工作量太大,以至于把它当作一个进程来处理是合理的,那我就说把它变成一个进程吧!
埃里克·利珀特

9

您的同事误会了,或者他知道C#语言规范所不知道的东西。

5.5变量引用的原子性

“以下数据类型的读写是原子的:bool,char,byte,sbyte,short,ushort,uint,int,float和引用类型。”

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

当然,您应该谨慎决定如何确定哪个线程应访存新数据,以最大程度地减少一次有多个线程这样做的风险。


3
@guffa:是的,我也读过。这留下了原始问题“引用分配是原子的,为什么需要Interlocked.Exchange(ref Object,Object)?” 未作答
char m

@zebrabox:你是什么意思?什么时候不呢?你会怎么做?
char m

@matti:当您必须作为原子操作读取和写入值时,这是必需的。
Guffa'2

您需要多久担心一次.NET中的内存确实没有正确对齐?互操作量大的东西?
Skurmedel'2

1
@zebrabox:规范没有列出该警告,它给出了一个非常清晰的声明。您是否有针对非内存对齐情况的引用,在这种情况下,引用读取或写入无法实现原子操作?似乎会违反规范中非常清晰的语言。
TJ Crowder

6

Interlocked.Exchange <T>

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

它会更改并返回原始值,它是无用的,因为您只想更改它,并且正如Guffa所说,它已经是原子的。

除非探查器证明它是应用程序的瓶颈,否则您应该考虑解除锁定,否则将更容易理解和证明您的代码正确。


3

Iterlocked.Exchange() 不只是原子的,它还照顾了内存可见性:

以下同步功能使用适当的屏障来确保内存顺序:

进入或离开关键部分的功能

向同步对象发出信号的功能

等待功能

互锁功能

同步和多处理器问题

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

  • 对于调用它的线程:
    • 不对指令进行重新排序(通过编译器,运行时或硬件)。
  • 对于所有线程:
    • 在该指令执行之前,不会发生对存储器的读取操作。
    • 该指令之后的所有读取操作都会看到该指令所做的更改。
    • 在该指令更改到达主存储器之后,将执行该指令之后的所有写操作(通过在完成该指令后将指令更改刷新到主存储器,而不是让硬件按时刷新自己的指令)。
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.