为什么volatile和MemoryBarrier不会阻止操作重新排序?

时间:2011-05-28 16:47:29

标签: c# .net multithreading

如果我正确理解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;
                    }
                }
            }
        }
    }

4 个答案:

答案 0 :(得分:10)

你没有清除测试之间的变量,所以(除了第一个之外的所有变量)最初a2b20 - 之前 Write已完成任何

Check可以获得a 初始 值(因此tempA2),然后Write可以进入,将b更改为10

现在Check会读取b(因此tempB10)。

Et瞧。不需要重新订购才能重现。

在两次运行之间将ab重置为0,我希望它会消失。

编辑:已确认; “按原样”我几乎立即得到了这个问题(< 2000次迭代);但通过添加:

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;

答案 1 :(得分:3)

我不认为这是重新订购。

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

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

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

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

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

答案 2 :(得分:0)

结果与重新排序,记忆障碍或易失性无关。需要所有这些结构来避免编译器或CPU重新排序指令的影响。

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

首先,请注意将并行启动多个Write()任务。它们由于Write()内部的lock()而按顺序运行,但是一个单一Check()方法可以读取由a任务的不同实例生成的bWrite()

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

这就是你所需要的:

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

答案 3 :(得分:-1)

  1. 如果您在MemoryBarrier中使用writer,为什么不在checker中执行此操作?将Thread.MemoryBarrier();放在int tempA = a;之前。

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