内存模型和ThreadPool

时间:2015-10-22 12:18:31

标签: c# concurrency task volatile memory-model

我有一个类NonVolatileTest:

public class NonVolatileTest 
{
    public bool _loop = true;
}

我有两个代码示例:

1:

private static void Main(string[] args)
{
    NonVolatileTest t = new NonVolatileTest();

    Task.Run(() => { t._loop = false; });

    while (t._loop) ;
    Console.WriteLine("terminated");

    Console.ReadLine();
}

2:

private static void Main(string[] args)
{
    NonVolatileTest t = new NonVolatileTest();

    Task.Run(() => { t._loop = false; });

    Task.Run(() =>
        {
            while (t._loop) ;
            Console.WriteLine("terminated");
        });

    Console.ReadLine();
}

在第一个示例中,所有工作都按预期工作,而'while'周期永远不会终止,但在第二个示例中,所有工作据称'_loop'字段都是不稳定的。

为什么?

PS。 VS 2013,.NET 4.5,x64 发布模式& Ctrl + F5

假设:

这个'bug'可能与TaskScheduler有关。我认为,在JIT为编译和运行完成第二个任务之前,第一个任务已经完成,因此JIT获取了更改的值。

2 个答案:

答案 0 :(得分:2)

根据C# 5 specification(在注释的C#4规范中可以找到相同的段落),在10.5.3节 - 易失性字段中,这是陈述:

  

当字段声明包含volatile修饰符时,该声明引入的字段是易失性字段。   对于非易失性字段,重新排序指令的优化技术可能会导致在不同步的情况下访问字段的多线程程序出现意外和不可预测的结果,例如lock-statement(第8.12节)提供的结果。这些优化可以由编译器,运行时系统或硬件执行。对于易失性字段,此类重新排序优化受到限制:

(我的重点)

所以这被证明是不可预测的(又名你的控制之外)。

这两段代码行为不同的事实可归结为将代码提升到生成对象上的方法(用于闭包)而不是将其提升之间的区别。

我的心灵代码阅读眼睛告诉我,这是第一种情况可能发生的事情:

  1. 该任务已启动,但在委托中的实际代码被称为
  2. 之前,这会产生开销
  3. 在调用委托之前,主程序继续并设法启动循环,单次读取控制变量,并继续重用其缓存副本。
  4. 委托最终被执行,但这对循环没有影响,因为它已经读过变量一次,并且没有再愿意这样做了。
  5. 在第二种情况下,由于第一种情况有效地“通过某些对象引用读取变量”而第二种情况有效地“通过this引用读取变量”这一事实略微改变了上述情况。 ,可能会施加差异。

    但真正的答案是你很容易使用优化器并编写不可预测的代码。

    不要惊慌,结果也是不可预测的。

    对代码进行微小的看似无关的更改可能会使优化器以不同的方式执行操作。

答案 1 :(得分:-1)

有一篇关于内存模型的文章:http://igoro.com/archive/volatile-keyword-in-c-memory-model-explained/

部分中有表格"内存模型和.NET操作" : "各种.NET操作如何与虚构线程缓存交互的表。"

正如我所看到的,普通阅读并没有刷新线程缓存。 而且我认为这意味着第二个任务是在第一个任务完成后开始的,因为第二个任务已经读了“假”。值。

下一个代码显示结果"终止:0",正如在这种情况下预期的那样。

这部分代码与第二个例子相同:

private static void Main(string[] args)
{
    NonVolatileTest t = new NonVolatileTest();

    Task.Run(() =>
    {
        var i = 0;
        while (t._loop)
        {
            i++;
        }
        Console.WriteLine("terminated: {0}", i);
    });
    //add delay here
    Task.Run(() => { t._loop = false; });

    Console.ReadLine();
}

事实证明,如果在第二个任务开始之前添加了Thread.Sleep(1000)延迟,第二个任务将读取未更改的值(true),因为第一个任务尚未完成,并且我们有与第一个例子相同的行为。