volatile 是否会阻止引入的读取或写入?

Does volatile prevent introduced reads or writes?

在 C# 中,volatile 关键字确保读取和写入分别具有获取和释放语义。但是,它是否说明了有关引入的读取或写入的内容?

例如:

volatile Thing something;
volatile int aNumber;

void Method()
{
    // Are these lines...
    var local = something;
    if (local != null)
        local.DoThings();

    // ...guaranteed not to be transformed into these by compiler, jitter or processor?
    if (something != null)
        something.DoThings(); // <-- Second read!



    // Are these lines...
    if (aNumber == 0)
        aNumber = 1;

    // ...guaranteed not to be transformed into these by compiler, jitter or processor?
    var temp = aNumber;
    if (temp == 0)
        temp = 1;
    aNumber = temp; // <-- An out-of-thin-air write!
}

这是 C# 规范1Execution Order 的说明:

Execution of a C# program proceeds such that the side effects of each executing thread are preserved at critical execution points. A side effect is defined as a read or write of a volatile field ...

The execution environment is free to change the order of execution of a C# program, subject to the following constraints:

...

The ordering of side effects is preserved with respect to volatile reads and writes ...

我当然会考虑引入新的副作用来改变副作用的顺序,但这里没有明确说明。


Link 的答案是列为草案的 C# 6 规范。 C# 5 规范不是草案,但无法在线获取,仅作为 download。就我在本节中看到的相同措辞。

不知你是否误解了volatile的意思。 Volatile 可与可作为原子操作读取或写入的类型一起使用。

没有 acquire/release ,只有编译时障碍和 运行 时重新排序以提供无锁 aquire/release语义(https://preshing.com/20120913/acquire-and-release-semantics/)。在非 x86 上,这可能需要 asm 中的屏障指令,但不需要锁定。

volatile 表示一个字段可能被其他线程修改,这就是为什么 read/writes 需要被视为原子而不是优化的原因。


你的问题有点含糊。

1/ 如果你的意思,编译器会不会转换:

var local = something;
if (local != null) local.DoThings();

进入:

if (something != null) something.DoThings();

那么答案是否定的。

2/ 如果你的意思是,“DoThings()”会在同一个对象上被调用两次:

var local = something;
if (local != null) local.DoThings();
if (something != null) something.DoThings(); 

那么答案大多是肯定的,除非另一个线程在调用第二个“DoThings()”之前更改了“something”的值。如果是这种情况,那么它可能会给您一个 运行 时间错误 - 如果在评估“if”条件之后和调用“DoThings”之前,另一个线程设置“something" 到 null 然后你会得到一个 运行 时间错误。我想这就是为什么你有你的“var local = something;”。

3/ 如果你的意思是下面会导致两次读取:

if (something != null) something.DoThings();

然后是的,一次读取条件,第二次读取它调用 DoThings()(假设 something 不为空)。如果它没有标记 volatile 那么编译器可能会通过一次读取来管理它。

无论如何,函数“DoThings()”的实现需要意识到它可以被多个线程调用,因此需要考虑合并锁和它自己的易失性成员的组合。

C# 规范中的措辞:

The ordering of side effects is preserved with respect to volatile reads and writes...

可能会被理解为暗示在volatile变量上不允许读写引入,但确实有歧义,要看"ordering."的意思 如果是指相对顺序现有访问,然后引入新的读取或写入不会改变这一点,因此它不会违反规范的这一部分。如果它指的是程序顺序中所有内存访问的确切位置,那么引入新的访问将违反规范。

This 文章说可能会引入对非 volatile 变量的读取,但没有明确说明是否不允许对 volatile 变量进行读取。

这篇Q/A讨论如何防止读取引入(但不讨论写入引入)。

this文章下的评论中,两名微软员工(至少在撰写评论时)明确声明不允许对volatile变量进行读写介绍。

Stephen Toub

"read introduction" is one mechanism by which a memory reordering might be introduced.

Igor Ostrovsky

Elsewhere in the C# specification, a volatile read is defined to be a "side effect". As a result, repeating the read of m_paused would be equivalent to adding another side effect, which is not allowed.

我认为我们可以从这些评论中得出结论,在 C# 中凭空引入副作用,任何类型的副作用,在代码的任何地方都是不允许的。

CLI 标准的相关引述在第 I.12.6.7 节中说明如下:

An optimizing compiler that converts CIL to native code shall not remove any volatile operation, nor shall it coalesce multiple volatile operations into a single operation.

据我所知,CLI 并没有明确提到引入新的副作用。