引用类型的字段如何是非易失性的?

时间:2014-03-06 08:34:54

标签: c# .net multithreading volatile

以下是MSDN关于volatile的说法:

  

volatile关键字表示字段可能被修改   多个线程同时执行。是的领域   声明的volatile不受编译器优化的限制   假设由单个线程访问。这确保了最多   字段中始终存在最新值

     

volatile关键字可以应用于以下类型的字段:   参考类型。

此状态表示默认情况下引用类型字段不易变 我认为将引用类型字段视为包含对象地址的值类型字段是可以的。然后它变得类似于int类型。 Joe Albahari gives some examples

但是!...与通常的值类型不同,GC在压缩堆并相应地更改引用时将对象移动到内存中。因此,“最新价值”必须始终存在。如果是这样,波动率的概念如何适用于参考类型?

3 个答案:

答案 0 :(得分:4)

  

此状态表示引用类型的字段不是易失性的   默认值。

不确定。默认情况下,任何字段都不会被视为volatile,因为volatile可能会带来相当大的性能成本。

  

我认为将引用类型字段视为值类型字段是可以的   包含对象的地址。然后它变得类似于int   类型。

假设这可以毫无问题地完成。所以呢?标量类型字段(例如intbool)默认情况下也不会被视为易失性。

  

与通常的值类型不同,GC在压缩时将对象移动到内存中   堆并相应地更改引用。因此'最多   最新的价值'必须始终存在。如果是这样,这个概念怎么样   波动率是否适用于参考类型?

您对volatile的效用感到有些困惑。它要解决的问题不仅是(A)最新值不存在(尽管volatile语义确实保证对值的任何写入都可以被观察到在抽象时间线中跟随它们的任何读取¹)。

除此之外,它还旨在解决情况(B),其中编译器假定它生成的代码是修改该值的唯一一方(或者该值未被修改为所有),这意味着它不会选择来从字段中读取值,而是使用它已经拥有的“缓存副本”。如果 值同时被第三方修改,这显然会导致程序使用错误的数据进行计算。

有关更多信息,请参阅Igor Ostrovsky撰写的这篇优秀analysis,其中(A)称为“处理器优化”,(B)称为“编译器优化”。


¹请注意,这不是一个严格的定义,而只是粗略的近似。

答案 1 :(得分:2)

易失性IS对参考字段很有用。

考虑这个程序:

using System;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    class Flag
    {
        public volatile bool Value;
    }

    sealed class Program
    {
        private void run()
        {
            flag.Value = true;
            Task.Factory.StartNew(resetFlagAfter1s);
            int x = 0;

            while (flag.Value)
                ++x;

            Console.WriteLine("Done");
        }

        private void resetFlagAfter1s()
        {
            Thread.Sleep(1000);
            flag = new Flag();
            flag.Value = false;
        }

        private Flag flag = new Flag();

        private static void Main()
        {
            new Program().run();
        }
    }
}

如果您运行此版本的RELEASE(不是调试版),它将永远不会终止(Visual Studio 2013,.Net 4.5x)

如果您将flag的声明更改为:

private volatile Flag flag = new Flag();

然后发布版本将终止。这证明volatile对于参考字段可能很重要。

答案 2 :(得分:1)

我认为Jon回答了这个问题。

我想补充一点,volatile关键字所做的“唯一”事情就是在字段上添加隐藏属性,并改变编译代码的方式(到IL)。

例如:

    static int x = 0;
    public static void Main(string[] args)
    {
        if (x == 0)
            x++;
    }

编译为

L_0000: ldsfld int32 Tests.Program::x
L_0005: brtrue.s L_0013
L_0007: ldsfld int32 Tests.Program::x
L_000c: ldc.i4.1 
L_000d: add 
L_000e: stsfld int32 Tests.Program::x
L_0013: ret 

如果你看一下这部分:

ldsfld x
brtrue.s L_0013
ldsfld x

(字面意思是“加载字段,如果为零,则跳转。否则,再次加载”)

前两个将在“伪asembler”

中转换为类似的东西
  • 在寄存器中加载x的值(mov)
  • 如果为零,则跳转到其他地方(jz)

对于第三个,我猜CLR会试图说“嘿,我已经知道我的寄存器包含x的值,为什么不跳过第三条指令?”

...“volatile”关键字阻止了这种优化。

将x定义更改为static volatile int x=0,然后获得:

L_0000: volatile. 
L_0002: ldsfld int32 modreq([mscorlib]System.Runtime.CompilerServices.IsVolatile) Tests.Program::x
L_0007: brtrue.s L_0019
L_0009: volatile. 
L_000b: ldsfld int32 modreq([mscorlib]System.Runtime.CompilerServices.IsVolatile) Tests.Program::x
L_0010: ldc.i4.1 
L_0011: add 
L_0012: volatile. 
L_0014: stsfld int32 modreq([mscorlib]System.Runtime.CompilerServices.IsVolatile) Tests.Program::x
L_0019: ret 

(与参考字段相同)

请参阅Opcodes.Volatile

  

指定当前在评估堆栈顶部的地址可能是易失性的,并且无法缓存读取该位置的结果或无法抑制到该位置的多个存储。

I recomand you to read Hans Passant excelent answer here