【问题标题】:Use of volatile keyword on the flag field在标志字段上使用 volatile 关键字
【发布时间】:2017-11-15 13:44:00
【问题描述】:

我尝试了解 volatile 在多线程上下文中的使用。以下代码来自网上another source of knowledge

class Program
{
    static string _result;
    //static volatile bool _done;
    static bool _done;

    static void SetVolatile()
    {
        // Set the string.
        _result = "Dot Net Perls";
        // The volatile field must be set at the end of this method.
        _done = true;
    }

    static void Main()
    {
        // Run the above method on a new thread.
        new Thread(new ThreadStart(SetVolatile)).Start();

        // Wait a while.
        Thread.Sleep(200);

        // Read the volatile field.
        if (_done)
        {
            Console.WriteLine(_result);
        }
    }
}

volatile 关键字的演示使用应该可以防止线程读取存储在缓存中的值。而不是这个,它应该检查一个实际值。

因此,如果没有 volatile 的 _done 应该仍然有一个 false 值(从缓存中读取)并且不应执行 Console.WriteLine 语句。

不幸的是,在没有 volatile 关键字的调试/发布模式下运行此代码总是会产生输出。这个特定示例的意义何在?

【问题讨论】:

  • 如果没有 volatile,编译器(静态和 JIT)可能优化读取,使赋值永远不会被看到,如果你严格阅读标准。但它不是必需,事实上,我认为.NET(过去或现在)的任何 JIT 编译器实际上都不会这样做,至少不是这种特定情况,不是在 x86/x64 .
  • 如果你想了解这些东西,那么开始阅读一些serious stuff也许吧?
  • "在我们开始之前,请注意这个例子并不理想,因为它在没有 volatile 修饰符的情况下也能正常工作。它仅用于说明 volatile 关键字的概念,而不是提供一个真实的例子” - 来自internet
  • 请参阅this answer 以了解使用volatile 实际上确实会产生影响的示例。

标签: c# multithreading volatile


【解决方案1】:

如前所述,不使用volatile 关键字并不意味着在所有情况下都需要缓存所有读取。它们可能被缓存,也可能不被缓存。但如果你想要更多可重现的例子,试试这个:

class Program {
    static string _result;
    //static volatile bool _done;
    static bool _done;
    static void SetVolatile() {
        // Set the string.
        _result = "Dot Net Perls";
        // The volatile field must be set at the end of this method.
        _done = true;
    }

    static void Main() {
        // Run the above method on a new thread.
        new Thread(new ThreadStart(SetVolatile)).Start();

        // prevent compiler to throw away empty while loop
        // by doing something in it
        int i = 0;
        while (!_done) {
            i++;
        }
        Console.WriteLine("done " + i);
    }
}

在这里,您在 while 循环中反复读取 _done,增加了它被缓存的可能性。程序应该以“done”消息终止,但不会,因为不会注意到从另一个线程更改为 _done

【讨论】:

    【解决方案2】:

    最好阅读 ECMA-335, §I.12.6
    要点是:

    • 程序可以优化(很多)
    • 将 CIL 转换为本机代码的优化编译器不应删除任何易失性操作也不应将多个易失性操作合并为单个操作


    因此,在这种情况下,您的代码可以进行优化。
    试试下面的代码:

    private bool flag = true;
    public void LoopReadHoistingTest()
    {
        Task.Run(() => { flag = false; });
        while (flag)
        {
          // Do nothing
        }
    }
    

    Debug 模式下(没有优化)它可以正常工作。在 Release 模式(带有优化)下,它将永远挂起,因为将读取移出循环是很常见的优化。
    但是,如果您将字段标记为 volatile(或使用 Volatile.Read 方法或某些 Interlocked 方法),它将起作用,因为在这种情况下禁止优化。

    在您的示例中(没有循环),Thread.Sleep 设置了一个隐式内存屏障(因为它不被禁止,并且它使代码的工作意外更少),因此它将从内存中读取值。但我没有看到任何规范说它必须做一个隐式内存屏障,所以在某些实现中它可能不是真的(或者我们必须在规范中找到它)。

    【讨论】:

      猜你喜欢
      • 2011-02-08
      • 1970-01-01
      • 2016-02-12
      • 2015-02-16
      • 1970-01-01
      • 2013-08-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多