【问题标题】:A reproducible example of volatile usage易失性使用的可重现示例
【发布时间】:2011-05-28 21:33:01
【问题描述】:

我正在寻找一个可重现的示例来演示 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# 中进行演示?

【问题讨论】:

  • 很容易(在 x86 上)通过强制读取来举一个重要的例子;够了吗?还是您只是指订购?
  • (这里,关于强制读取 - 不像订购那么强大:stackoverflow.com/questions/458173/…
  • 正如我所说,我看到了那些帖子。我对重新排序示例感兴趣。
  • MSDN 示例与重新排序无关。这是关于“完成”领域的优化。
  • 您可能还想关注 AMD 和 Itanium (iA64) 芯片:albahari.com/threading/part4.aspx#_The_volatile_keyword

标签: c# .net multithreading


【解决方案1】:

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

C# 语言规范定义了 volatile 的行为,quoted here 由 Eric Lippert 编写。 “释放”和“获取”语义仅在具有弱内存模型的处理器内核上才有意义。这类处理器在市场上的表现并不好,可能是因为它们编程起来非常痛苦。您的代码在 Titanium 上运行的可能性微乎其微。

C# 语言规范定义的特别糟糕之处在于它根本没有提及 真正 发生了什么。声明变量 volatile 可防止抖动优化器优化代码以将变量存储在 cpu 寄存器中。这就是为什么 Marc 链接的代码挂起的原因。这只会发生在当前的 x86 抖动中,这是另一个强烈暗示 volatile 确实是一个抖动实现细节。

volatile 的糟糕语义有着悠久的历史,它来自 C 语言。谁的代码生成器也很难正确处理。这是一个有趣的report about it (pdf)。它可以追溯到 2008 年,这是一个 30 多年的好机会来让它正确。或者错了,当代码优化器忘记变量是易变的时,这就会失败。未优化的代码永远不会有问题。值得注意的是,“开源”版本的 .NET (SSLI20) 中的抖动完全忽略了 IL 指令。也可以说 x86 抖动的当前行为是一个错误。我认为是,将其撞入故障模式并不容易。但没有人能争辩说它实际上一个错误。

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

【讨论】:

  • 好文章。我自己倾向于Interlocked 而不是volatile - 至少我可以完全定义行为;p
  • 好答案。 Joe Duffy 也有意见:Volatile is evil
  • 它没有回答我的问题。正如 Joe Duffy 所说,我正在寻找能够证明 volatile 操作“确保不会对具有外部副作用的内存操作进行重新排序”的示例。
  • 嗯,我做到了。先找一台cpu内存模型弱的机器。在废品堆上应该不难找到带有安腾的一个。等到冬天,我听说它们是很好的空间加热器。我认为 ARM 内核也很弱,这需要配备双核 cpu 的 Windows 7 手机。我认为你也必须等待,微软的 iPad 克隆可能是第一个。
【解决方案2】:

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

这里发生的情况是while 循环中stop 的读取被重新排序,以便在省略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 及更高版本中重现此行为,但不能在早期版本中重现。

【讨论】:

  • 太棒了!!!!我每次都能在 RELEASE 模式下重现!它可以解释我在编码生涯中遇到的很多问题!谢谢!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2023-03-21
  • 1970-01-01
  • 1970-01-01
  • 2016-05-31
  • 1970-01-01
  • 1970-01-01
  • 2013-11-13
相关资源
最近更新 更多