【问题标题】:How fields of reference type can be nonvolatile?引用类型的字段如何可以是非易失性的?
【发布时间】:2014-03-06 08:34:53
【问题描述】:

这是MSDNvolatile 的评价:

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

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

此状态意味着默认情况下引用类型的字段不是易失性的。
我认为可以将引用类型字段视为包含对象地址的值类型字段。然后它变成类似于int 类型。乔·阿尔巴哈里gives some examples.

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

【问题讨论】:

    标签: c# .net multithreading volatile


    【解决方案1】:

    这种状态意味着引用类型的字段不是易失性的 默认。

    当然。默认情况下,没有字段被视为volatile,因为volatile 可能会带来相当大的性能成本。

    我觉得可以把引用类型的字段当作值类型的字段 包含对象的地址。然后它变得类似于 int 输入。

    假设这可以毫无问题地完成。所以呢?默认情况下,intbool 等标量类型的字段也不会被视为 volatile。

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

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

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

    有关更多信息,您可以参考 Igor Ostrovsky 的这篇出色的 analysis,其中 (A) 称为“处理器优化”,(B) 称为“编译器优化”。


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

    【讨论】:

    • '编译器假定它是唯一修改该值的一方'我认为这令人困惑。修改值是运行时,编译器在编译时工作。编译器无法修改这些值,因为程序尚未运行。我建议改写。
    • @zespri:编译器可以“证明”值没有被修改,例如代码中的任何地方都没有写入它。它不需要运行程序来执行此操作。你还觉得措辞不对吗?
    • 我建议类似于“...编译器假定生成的代码是唯一修改该值的一方(或根本没有修改该值),这意味着它不会选择生成代码来读取值......”但可能只是我。明确一点:我明白你在说什么,但我看到了我想指出的含糊之处。
    • @zespri:这听起来很合理,我进行了编辑以匹配。感谢您的反馈。
    【解决方案2】:

    Volatile 对参考字段很有用。

    考虑这个程序:

    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 对于引用字段可能很重要。

    【讨论】:

    • @voroninp 编译的时候有没有使用Release模式?
    • @Dirk 是的。发布-AnyCPU, VS2013, Win7, Intel Core i7, M620 2.67GHz x64
    • @voroninp 对不起,你是对的。我正在测试 LINQpad 中代码的略微修改版本。如果您从 Flag 类的布尔字段中删除 volatile,它将不会终止。如果您认为这是因为它是一个值类型,只需将其更改为 object 并测试 null。
    • @Dirk Alas..当它是对象时仍然终止。即使是布尔值。这很奇怪。
    • @voroninp GC 机制(其中之一,它很复杂)涉及停放所有涉及同步的线程,这又涉及内存屏障。
    【解决方案3】:

    我认为乔恩回答了这个问题。

    我要补充一点,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
    

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

    前两个将在“伪汇编程序”中翻译成类似这样的内容

    • 在寄存器中加载 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

    【讨论】:

      猜你喜欢
      • 2018-01-18
      • 1970-01-01
      • 2023-02-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-09-28
      • 2017-02-18
      相关资源
      最近更新 更多