【问题标题】:Why volatile and MemoryBarrier do not prevent operations reordering?为什么 volatile 和 MemoryBarrier 不会阻止操作重新排序?
【发布时间】:2011-05-29 03:18:20
【问题描述】:

如果我正确理解 volatile 和 MemoryBarrier 的含义,那么下面的程序将永远无法显示任何结果。

每次我运行它时,它都会捕获写入操作的重新排序。我在 Debug 或 Release 中运行它并不重要。我将它作为 32 位或 64 位应用程序运行也没关系。

为什么会这样?

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

    namespace FlipFlop
    {
        class Program
        {
            //Declaring these variables as volatile should instruct compiler to 
            //flush all caches from registers into the memory.
            static volatile int a;
            static volatile int b;

            //Track a number of iteration that it took to detect operation reordering.
            static long iterations = 0;

            static object locker = new object();

            //Indicates that operation reordering is not found yet.
            static volatile bool continueTrying = true;

            //Indicates that Check method should continue.
            static volatile bool continueChecking = true;

            static void Main(string[] args)
            {
                //Restarting test until able to catch reordering.
                while (continueTrying)
                {
                    iterations++;
                    var checker = new Task(Check);
                    var writter = new Task(Write);
                    lock (locker)
                    {
                        continueChecking = true;
                        checker.Start();

                    }
                    writter.Start();
                    checker.Wait();
                    writter.Wait();
                }
                Console.ReadKey();
            }

            static void Write()
            {
                //Writing is locked until Main will start Check() method.
                lock (locker)
                {
                    //Using memory barrier should prevent opration reordering.
                    a = 1;
                    Thread.MemoryBarrier();
                    b = 10;
                    Thread.MemoryBarrier();
                    b = 20;
                    Thread.MemoryBarrier();
                    a = 2;

                    //Stops spinning in the Check method.
                    continueChecking = false;
                }
            }

            static void Check()
            {
                //Spins until finds operation reordering or stopped by Write method.
                while (continueChecking)
                {
                    int tempA = a;
                    int tempB = b;

                    if (tempB == 10 && tempA == 2)
                    {
                        continueTrying = false;
                        Console.WriteLine("Caught when a = {0} and b = {1}", tempA, tempB);
                        Console.WriteLine("In " + iterations + " iterations.");
                        break;
                    }
                }
            }
        }
    }

【问题讨论】:

  • b/c 这是他们的想法。内存屏障(写)只是确保所有操作都被刷新,因此以下是有序的部分屏障。
  • 代码中最有趣的是删除所有Thread.MemoryBarrier(); 行可以解决您的问题=)
  • @Mikant:不,那没有解决问题。它只是使它非常不可能。让它运行几天,它仍然可能发生。

标签: c# .net multithreading


【解决方案1】:

你没有清理测试之间的变量,所以(除了第一个)最初 a 是 2 和 b 是 20 - 之前 Write 有做了任何事情。

Check可以得到a的那个initial值(所以tempA就是2),然后Write可以进去,得到至于将b 更改为10。

现在Check 读取b(所以tempB 是10)。

等等,瞧。无需重新订购即可重新制作。

在运行之间将a 和b 重置为0,我希望它会消失。

编辑:确认; “按原样”我几乎立即得到了问题(

while (continueTrying)
{
    a = b = 0; // reset <======= added this

然后它会循环任意时间而不会出现任何问题。

或者作为一个流程:

Write                   A=  B=        Check

(except first run)      2   20
                                      int tempA = a;
a = 1;                  1   20
Thread.MemoryBarrier();
b = 10;                 1   10
                                      int tempB = b;

【讨论】:

  • @Dennis 没问题;我认为他删除然后取消删除,所以它不存在。接受他是正确的做法
【解决方案2】:

我不认为这是重新订购。

这段代码根本不是线程安全的:

 while (continueChecking)
 {
     int tempA = a;
     int tempB = b;
     ...

我认为这种情况是可能的:

  1. int tempA = a; 使用最后一个循环的值执行 (a == 2)
  2. 写入线程有上下文切换
  3. b = 10 循环停止
  4. 有一个上下文切换到检查线程
  5. int tempB = b; 以 b == 10 执行

我注意到对 MemoryBarrier() 的调用增加了这种情况的发生机会。可能是因为它们会导致更多的上下文切换。

【讨论】:

  • 你怀疑自己并删除了吗?
  • @Marc:我有正确的答案/见解,但在第二次阅读时,我被自己的错字弄糊涂了:}。
【解决方案3】:

结果与重新排序、内存条或易失性无关。需要所有这些构造来避免编译器或 CPU 对指令重新排序的影响。

但即使假设完全一致的单 CPU 内存模型且没有编译器优化,此程序也会产生相同的结果。

首先,请注意将有多个Write() 任务并行启动。由于 Write() 中的 lock(),它们按顺序运行,但是一个信号 Check() 方法可以读取由 Write() 任务的不同实例产生的 a 和 b。

因为Check() 函数与Write 函数没有同步 - 它可以在两个任意不同的时刻读取a 和b。您的代码中没有任何内容可以阻止Check() 在某一时刻读取由先前的Write() 生成的a,然后在另一时刻读取由关注Write() 生成的b。首先,您需要在Check() 中进行同步(锁定),然后您可能(但在这种情况下可能不是)需要内存屏障和易失性来解决内存模型问题。

这就是你所需要的:

        int tempA, tempB;
        lock (locker)
        {
            tempA = a;
            tempB = b;
        }

【讨论】:

  • 看起来比这更有趣。如果没有重新排序,什么情况下会给出 tempA/tempB 的这些值?请注意,每个测试只有一次写入
  • (锁只是为了延迟访问,因此写入不会太快发生;碰巧,它不一定会这样做,因为在开始和实际之间可能存在延迟开始 - 但它似乎已经足够接近了)
  • @Marc - Checker 在 Writer 之前启动,因此它有机会观察 Writer 所做的所有写入。 Writer 中的 MemoryBarrier 只会让事情变得更糟,因为它增加了 Checker 看到所有中间值的变化
  • OP 并没有试图讨论互斥锁 - 它确实专注于预期的重新排序预防。最后的“锁定”建议真的错过了他试图说明的内容,IMO。同样,问题中的锁 不打算 提供互斥锁;它只是为了延迟写入,直到阅读器开始。
  • “将有多个 Write() 任务并行启动”——这是非常错误的。 1 写/迭代。
【解决方案4】:
  1. 如果你在writer 中使用MemoryBarrier,为什么不在checker 中这样做呢?将Thread.MemoryBarrier(); 放在int tempA = a; 之前。

  2. 多次调用Thread.MemoryBarrier(); 会阻止该方法的所有优点。在a = 1;之前或之后只调用一次。

【讨论】:

  • 这并不能真正解释发生了什么。这些建议如何解决 .NET 内存模型方面的问题?
  • @dtb 在您编辑我的帖子并删除我的帖子可能是@Dennis 的线索之前,这更清楚了......他的代码中没有发生任何神秘的事情。 .NET 内存模型没有问题。一切都按书面规定进行。所以我认为丹尼斯能够在我的 .writings 之后得到问题的答案。
  • 也许丹尼斯可以根据你的线索得到答案,但你为什么不直接把答案提供给大家看呢?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2019-06-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-12-12
  • 1970-01-01
相关资源
最近更新 更多