【问题标题】:Code example proven to fail w/o volatile代码示例被证明在没有 volatile 的情况下失败
【发布时间】:2014-07-03 14:10:58
【问题描述】:

下面是一个 C# 代码示例,它是至少在 Mac OS X 10.9 上对损坏的 Java 代码(已被证明是损坏的(即第二个线程可能无法观察到 sharedValue 值的变化)的逐字翻译), Java 1.8(64 位),Arrandale(1 个套接字 x 2 个内核 x 2 HT = 4 个硬件线程)):

using System;
using System.Threading;

class ThreadTest {
    /* volatile */ private int sharedValue;

    private void RunAsync() {
        while (this.sharedValue == 0);
    }

    private bool Test() {
        Thread t = new Thread(this.RunAsync);
        t.IsBackground = true;
        t.Start();

        Thread.Sleep(10);

        // Yes I'm aware the operation is not atomic
        this.sharedValue++;

        t.Join(10);
        bool success = !t.IsAlive;
        if (!success) {
            Console.Write('.');
        }
        return success;
    }

    static void Main() {
        long failureCount = 0L;
        const long testCount = 10000L;
        for (long i = 0; i < testCount; i++) {
            if (!new ThreadTest().Test()) {
                failureCount++;
            }
        }
        Console.WriteLine();
        Console.WriteLine("Failure rate: " + 100.0 * failureCount / testCount + "%");
    }
}

令人惊讶的是,无论我在 .NET 4.0/Windows XP(32 位)上运行上述 C# 代码多少次,我都没有观察到一次失败。在 Mono(64 位)、Mac OS X 上运行时也没有任何故障。在这两种情况下,我只看到一个 CPU 内核处于忙碌状态。

您能否推荐一个 C# 代码示例,该示例错误地使用了共享变量并失败,除非该变量被标记为 volatile

【问题讨论】:

  • Eric Lippert 对此有一些信息。他和 Joe Duffy 似乎暗示 x86 中的 CLR 上没有这种情况。 blog.coverity.com/2014/03/26/reordering-optimizationsjoeduffyblog.com/2010/12/04/sayonara-volatile
  • 感谢@MobyDisk 的链接。尽管如此,我已经在 Smithfield(1 插槽 x 1 核 x 2 HT)和 Arrandale(1 插槽 x 2 核 x 2 HT)的 .NET 4.0/Windows XP(32 位)上发现了上述代码,在分别在裸机和虚拟机中。关键是按照下面 Matthew Watson 的建议运行 release 构建。

标签: c# multithreading concurrency volatile


【解决方案1】:

尝试运行以下程序的 RELEASE 构建(不要从调试器中运行它,否则演示将无法运行 - 所以通过“调试 | 不调试就开始”运行发布构建):

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

namespace Demo
{
    internal class Program
    {
        private void run()
        {
            Task.Factory.StartNew(resetFlagAfter1s);
            int x = 0;

            while (flag)
                ++x;

            Console.WriteLine("Done");
        }

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

        private volatile bool flag = true;

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

程序将在一秒钟后终止。

现在从行中删除volatile

private volatile bool flag = true; // <--- Remove volatile from this

这样做之后,程序将永远不会终止。 (在 Windows 8 x64、.Net 4.5 上测试)

但是请注意,在某些情况下,使用Thread.MemoryBarrier() 而不是声明变量volatile 更合适,即:

while (flag)
{
    Thread.MemoryBarrier();
    ++x;
}

欲了解更多信息,请参阅http://blogs.msdn.com/b/brada/archive/2004/05/12/130935.aspx

【讨论】:

  • 谢谢马修!关键点是运行 RELEASE 构建(我正在运行 DEBUG 构建,因此无法观察到错误,尽管二进制文件是在调试器之外运行的)。
  • 很好奇在这种情况下优化了什么。反编译的代码看起来几乎相同,只是它将 x 重命名为 num。如果我将 x 从方法体移动到类级别,它不再锁定。如果我在循环之后检查 x,它不再锁定。看起来它不会费心将 x 存储在内存中,只是更新 regsiter 可能是,但是一旦我让它在方法之外可见,或者在它正确地不再优化它之后检查它。为什么即使标志是公开的,它也会优化标志的检查?看起来更像是编译器中的错误,而不是预期的行为
【解决方案2】:

这应该会陷入无限循环(我现在无法测试)

public class Test
{
  private bool loop = true;

  public static void Main()
  {
    Test test = new Test();
    Thread thread = new Thread(DoStuff);
    thread.Start(test);

    Thread.Sleep(1000);

    test.loop = false;
    Console.WriteLine("loop is now false");
  }

  private static void DoStuff(object o) {
    Test test = (Test)o;
    Console.WriteLine("Entering loop");
    while (test.loop) {

    }
    Console.WriteLine("Exited loop");
  }

}

【讨论】:

    猜你喜欢
    • 2015-09-06
    • 2019-01-19
    • 2020-09-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-02-02
    相关资源
    最近更新 更多