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

时间:2018-10-31 08:55:31

标签: c# language-lawyer compiler-optimization volatile lock-free

在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!
}

3 个答案:

答案 0 :(得分:4)

这是C#规范 1 关于Execution Order的内容:

  

继续执行C#程序,以使每个执行线程的副作用都保留在关键执行点上。 副作用定义为对易失性字段的读取或写入...

     

执行环境可以自由更改C#程序的执行顺序,但要遵守以下约束:

     

...

     

相对于易失性读写,保留了副作用的顺序...

我当然会认为引入新的副作用会改变副作用的顺序,但此处并未明确指出。


答案中的链接是列为“草稿”的C#6规范。 C#5规范不是草案,但不能在线使用,只能作为download使用。就本节中所见,措词完全相同。

答案 1 :(得分:2)

C#规范中的措辞:

  

关于挥发物,副作用的顺序得以保留   读写...

可以解释为暗示不允许对volatile变量进行读写介绍,但这确实是模棱两可的,并且取决于“排序”的含义。如果它指的是现有访问的相对顺序,则引入新的读取或写入操作不会更改此内容,因此不会违反规范的这一部分。如果它以程序顺序指代所有内存访问的确切位置,那么引入新的访问将违反规范。

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

Q/A讨论了如何防止阅读简介(但没有关于书写简介的讨论)。

this下的注释中,两名Microsoft员工(至少在撰写注释时)明确声明不允许对volatile变量进行读写介绍。

  

斯蒂芬·图布

     

“阅读介绍”是一种对内存进行重新排序的机制   可能会引入。

     

伊戈尔·奥斯特洛夫斯基

     

在C#规范的其他地方,易失性读定义为   “副作用”。结果,重复读取m_paused将是   等同于添加了另一种副作用,这是不允许的。

我认为我们可以从这些注释中得出结论,即在C#中引入了副作用,不允许在代码中的任何位置出现任何形式的副作用。

CLI标准的相关引用在I.12.6.7节中声明如下:

  

不能将CIL转换为本机代码的优化编译器   删除任何易失性操作,也不应合并多个易失性   操作合并为一个操作。

据我所知,CLI没有明确谈论引入新的副作用。

答案 2 :(得分:-1)

我想知道您是否误解了volatile的含义。易失性可以与可以作为原子动作读取或写入的类型一起使用。

没有锁的获取/释放 ,仅是编译时和运行时重新排序的障碍,以提供无锁的获取/释放语义(https://preshing.com/20120913/acquire-and-release-semantics/)。在非x86上,这可能需要在自动柜员机中输入屏障指令,但不需锁定。

volatile表示某个字段可能被其他线程修改,这就是为什么读/写需要被视为原子级而不是最佳化的原因。


您的问题有点模棱两可。

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(); 

然后答案通常是肯定的,除非另一个线程在调用第二个“ something”之前更改了“ DoThings()”的值。如果是这种情况,则可能会给您带来运行时错误-如果在评估“ if”条件之后且在调用“ DoThings”之前,另一个线程设置了“ something”到null,则将出现运行时错误。我认为这就是为什么您拥有“ var local = something;”的原因。

3 /如果您的意思是以下内容将导致两次读取:

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

然后是,一次读取条件,第二次读取条件调用DoThings()(假设something不为null)。如果未将其标记为volatile,则编译器可以通过一次读取来管理它。

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