118

在我的多线程 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” 我应该怎么想?

4

4 回答 4

197

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

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

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

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

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

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

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

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

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

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

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

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

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

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

于 2010-02-03T16:20:07.780 回答
9

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

变量引用的原子性

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

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

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

于 2010-02-03T13:32:42.857 回答
7

互锁交换<T>

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

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

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

于 2010-02-03T13:40:48.067 回答
4

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

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

进入或离开临界区的函数

信号同步对象的函数

等待函数

联锁功能

同步和多处理器问题

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

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