9

我很困惑。我之前的问题的答案似乎证实了我的假设。但正如此处所述, volatile 不足以确保 .Net 中的原子性。MSIL 中的增量和赋值等操作不会直接转换为单个本地 OPCODE,或者多个 CPU 可以同时读取和写入相同的 RAM 位置。

澄清:

  1. 我想知道写入和读取是否在多个 CPU 上是原子的?
  2. 我明白 volatile 是什么意思。但够了吗?如果我想获取其他 CPU 写入的最新值,是否需要使用联锁操作?
4

7 回答 7

10

Herb Sutter 最近写了一篇关于volatile原生 C++ 及其真正含义(它如何影响内存访问顺序和原子性)的文章。.NET 和 Java 环境。这是一个很好的阅读:

于 2009-02-05T18:33:58.813 回答
6

.NET 中的 volatile 确实可以访问变量 atomic。

问题是,这通常是不够的。如果你需要读取变量怎么办,如果它为0(表示资源空闲),你将它设置为1(表示它被锁定,其他线程应该远离它)。

读取 0 是原子的。写 1 是原子的。但是在这两个操作之间,任何事情都可能发生。您可能会读取 0,然后在您可以写入 1 之前,另一个线程跳入,读取 0,然后写入 1。

但是,.NET 中的 volatile确实保证了访问变量的原子性。它只是不能保证依赖于多次访问的操作的线程安全。(免责声明:C/C++ 中的 volatile 甚至不能保证这一点。正如你所知。它要弱得多,并且有时是错误的来源,因为人们认为它保证原子性 :))

因此,您还需要使用锁,将多个操作组合为一个线程安全块。(或者,对于简单的操作Interlocked,.NET 中的操作可以解决问题)

于 2009-02-05T18:08:47.650 回答
5

我可能会在这里跳枪,但在我看来,您似乎在这里混淆了两个问题。

一个是原子性,在我看来,这意味着单个操作(可能需要多个步骤)不应该与另一个这样的单个操作发生冲突。

另一个是波动性,这个值预计何时会发生变化,以及为什么会发生变化。

拿第一个。如果你的两步操作需要你读取当前值,修改它,然后写回,你肯定会想要一个锁,除非整个操作可以被转换成一个可以在一个 CPU 上运行的指令。单个缓存行数据。

但是,第二个问题是,即使您正在执行锁定操作,其他线程也会看到什么。

.NET 中的volatile字段是编译器知道可以随时更改的字段。在单线程世界中,变量的更改是在顺序指令流中的某个时间点发生的,因此编译器知道它何时添加了更改它的代码,或者至少当它调用外部世界时可能会也可能不会更改它,因此一旦代码返回,它可能与调用之前的值不同。

这种知识允许编译器在循环或类似代码块之前将字段中的值一次提升到寄存器中,并且永远不会从该特定代码的字段中重新读取值。

但是,使用多线程可能会给您带来一些问题。一个线程可能已经调整了这个值,而另一个线程由于优化,在一段时间内不会读取这个值,因为它知道它没有改变。

因此,当您标记一个字段时,volatile您基本上是在告诉编译器它不应该假定它在任何时候都具有 this 的当前值,除非在每次需要该值时抓取快照。

锁解决了多步操作,易失性处理了编译器如何将字段值缓存在寄存器中,它们一起将解决更多问题。

另请注意,如果一个字段包含无法在单个 cpu 指令中读取的内容,您很可能也希望锁定对它的读取访问权限。

例如,如果您在 32 位 cpu 上并写入 64 位值,则该写入操作将需要两个步骤才能完成,并且如果另一个 cpu 上的另一个线程在步骤 2 之前设法读取 64 位值完成后,它会得到一半以前的价值和一半新的,很好地混合在一起,这比得到一个过时的更糟糕。


编辑:回答评论,volatile保证读/写操作的原子性,这在某种程度上是正确的,因为volatile关键字不能应用于大于 32 位的字段,实际上使字段单cpu 指令可在 32 位和 64 位 cpu 上读/写。是的,它会尽可能地防止将值保存在寄存器中。

所以部分注释是错误的,volatile不能应用于 64 位值。

还要注意,它volatile有一些关于读/写重新排序的语义。

有关相关信息,请参阅MSDN 文档C# 规范,可在此处找到,第 10.5.3 节。

于 2009-02-05T18:12:27.330 回答
1

在硬件级别上,多个 CPU 永远不能同时写入同一个原子 RAM 位置。原子读/写操作的大小取决于 CPU 架构,但在 32 位架构上通常为 1、2 或 4 个字节。但是,如果您尝试回读结果,则总是有可能另一个 CPU 已写入其间的同一 RAM 位置。在低级别上,自旋锁通常用于同步对共享内存的访问。在高级语言中,这种机制可以称为例如临界区。

volatile 类型只是确保变量在更改时立即写回内存(即使该值要在同一个函数中使用)。如果稍后要在同一函数中重用该值,编译器通常会将值尽可能长时间地保存在内部寄存器中,并在所有修改完成或函数返回时将其存储回 RAM。易失性类型在写入硬件寄存器时非常有用,或者当您想确保将值存储回 RAM 中时,例如多线程系统。

于 2009-02-05T18:42:11.700 回答
0

您的问题并不完全有意义,因为volatile 指定了读取的发生方式,而不是多步过程的原子性。我的车也不会修剪我的草坪,但我尽量不让它靠着它。:)

于 2009-02-05T18:08:56.463 回答
0

问题来自于变量值的基于寄存器的兑现副本。

读取值时,cpu 将首先查看它是否在寄存器中(快),然后再检查主内存(慢)。

Volatile 告诉编译器尽快将值推送到主存储器,而不是信任缓存的寄存器值。它仅在某些情况下有用。

如果您正在寻找单个操作代码写入,则需要使用 Interlocked.Increment 相关方法。但是它们在单个安全指令中可以做的事情相当有限。

最安全和最可靠的赌注是 lock() (如果你不能做一个 Interlocked.*)

编辑:如果写入和读取处于锁定或互锁。* 语句中,则它们是原子的。根据您的问题,仅挥发性是不够的

于 2009-02-05T18:12:13.303 回答
-1

Volatile 是一个编译器关键字,它告诉编译器要做什么。它不一定转化为(本质上)原子性所需的总线操作。这通常由操作系统决定。

编辑:澄清一下,如果你想保证原子性, volatile 是远远不够的。或者更确切地说,由编译器决定是否足够。

于 2009-02-05T18:08:17.180 回答