挥发性使用的可重现的例子

时间:2011-05-28 21:33:02

标签: c# .net multithreading

我正在寻找一个可重现的示例,可以演示volatile关键字的工作原理。我正在寻找有用的东西"错误"没有标记为易变的变量,并正确地工作"用它。

我的意思是一些示例,它将证明执行期间的写/读操作顺序与变量未标记为volatile时的预期顺序不同,并且当变量未标记为volatile时不会有所不同。

我认为我得到了一个例子但是在其他人的帮助下我意识到它只是一段错误的多线程代码。 Why volatile and MemoryBarrier do not prevent operations reordering?

我还发现了一个链接,它演示了volatile对优化器的影响,但它与我正在寻找的不同。它表明对标记为volatile的变量的请求不会被优化出来。How to illustrate usage of volatile keyword in C#

这是我到目前为止的地方。此代码未显示任何读/写操作重新排序的迹象。我正在寻找一个会展示的。

    using System;
    using System.Threading;
    using System.Threading.Tasks;
    using System.Runtime.CompilerServices;

    namespace FlipFlop
    {
        class Program
        {
            //Declaring these variables 
            static byte a;
            static byte 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++;
                    a = 0;
                    b = 0;
                    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)
                {
                    WriteInOneDirection();
                    WriteInOtherDirection();

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

            [MethodImpl(MethodImplOptions.NoInlining)]
            static void WriteInOneDirection(){
                a = 1;
                b = 10;
            }

            [MethodImpl(MethodImplOptions.NoInlining)]
            static void WriteInOtherDirection()
            {
                b = 20;
                a = 2;
            }

            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;
                    }
                }
            }
        }
    }

编辑:

据我所知,导致重新排序的优化可以来自JITer或来自硬件本身。我可以改写一下我的问题。 JITer或x86 CPU是否重新排序读/写操作?如果有的话,有没有办法在C#中演示它?

2 个答案:

答案 0 :(得分:17)

volatile 的确切语义是抖动实现细节。编译器发出Opcodes.Volatile IL指令,您可以访问声明为volatile的变量。它做了一些检查以验证变量类型是否合法,你不能声明大于4字节volatile的值类型,但这就是降压停止的地方。

C#语言规范定义了Eric Lippert的 volatile quoted here的行为。 'release'和'acquire'语义只对具有弱内存模型的处理器内核才有意义。这类处理器在市场上表现不佳,可能是因为它们是如此巨大的编程难题。您的代码在Titanium上运行的可能性很小甚至没有。

C#语言规范定义的特别之处在于它根本没有提到真正发生的事情。声明变量volatile会阻止抖动优化器优化代码以将变量存储在cpu寄存器中。这就是为什么Marc链接的代码悬而未决。这只会发生在当前的x86抖动中,另一个强烈暗示 volatile 实际上是一个抖动的实现细节。

volatile 的语义不佳有着丰富的历史,它来自C语言。谁的代码生成器也很难解决问题。这是一个有趣的report about it (pdf)。它的历史可以追溯到2008年,这是一个30年以上的良好机会。或者错误,当代码优化器忘记变量是易失性时,这会变得很糟糕。未经优化的代码从未出现过问题。值得注意的是,“开源”版.NET(SSLI20)中的抖动完全忽略了IL指令。还可以说x86抖动的当前行为是一个错误。我认为,将其推入故障模式并不容易。但没有人可以说它实际上 是一个错误。

写入在墙上,只有在存储在存储器映射寄存器中时才声明变量volatile。关键字的初衷。您在C#语言中遇到此类用法的几率应该非常小,类似的代码属于设备驱动程序。最重要的是,从不假设它在多线程场景中很有用。

答案 1 :(得分:4)

您可以使用此示例演示使用和不使用volatile的不同行为。 此示例必须使用Release版本进行编译,并在调试器 1 之外运行。通过在volatile标记中添加和删除stop关键字进行试验。

这里发生的是stop循环中while的读取被重新排序,以便在循环之前发生,如果省略volatile。即使在主线程将stop标志设置为true之后,这也可以防止线程结束。

class Program
{
    static bool stop = false;

    public static void Main(string[] args)
    {
        var t = new Thread(() =>
        {
            Console.WriteLine("thread begin");
            bool toggle = false;
            while (!stop)
            {
                toggle = !toggle;
            }
            Console.WriteLine("thread end");
        });
        t.Start();
        Thread.Sleep(1000);
        stop = true;
        Console.WriteLine("stop = true");
        Console.WriteLine("waiting...");

        // The Join call should return almost immediately.
        // With volatile it DOES.
        // Without volatile it does NOT.
        t.Join(); 
    }
}

还应注意,对此示例的细微更改可降低其再现性的可能性。例如,添加Thread.Sleep(可能是为了模拟线程交错)本身会引入内存屏障,从而引入volatile关键字的类似语义。我怀疑Console.WriteLine引入了隐式内存屏障或以其他方式阻止抖动使用指令重新排序操作。如果你开始搞乱这个例子,请记住这一点。


1 我认为2.0之前的框架版本不包括此重新排序优化。这意味着您应该能够使用2.0及更高版本重现此行为,但不能恢复早期版本。