【问题标题】:Memory Model and ThreadPool内存模型和线程池
【发布时间】:2015-10-22 12:18:31
【问题描述】:

我有一类 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。 与 2013、.NET 4.5、x64 释放模式 & Ctrl + F5

假设:

此“错误”可能与 TaskScheduler 有关。我认为,在 JIT 将第二个任务用于编译和运行之前,第一个任务已经完成,所以 JIT 取了更改后的值。

【问题讨论】:

  • 字段未标记为 volatile 的事实并不意味着编译器/优化器/抖动不会重新读取它。这也可能很简单,在第一个示例中,代码在任务设法将布尔标志设置为 false 之前设法竞相读取布尔标志,但在第二种情况下,第二个任务在启动时也有开销运行,这意味着第一个任务首先设法将布尔值设置为 false。尝试在第一个任务的开头添加睡眠并重新测试。
  • 另外,第一个代码显然是错误的。首先,您永远不会将loop 设置为true,它在一个地方命名为loop,在另一个地方命名为_loop。你能发布正确的代码吗?
  • Lasse V. Karlsen,我在开始第二个任务之前延迟修复它:Thread.Sleep(1000)。
  • 这个“错误”可能与 TaskScheduler 有关。我认为,在 JIT 将第二个任务用于编译和运行之前,第一个任务已经完成,所以 JIT 取了更改后的值。
  • 如果您“解决”了您的问题,或者发布一个解释如何解决问题的答案并在两天限制过后接受它,或者删除问题,不要只在“已解决”部分进行编辑回答你的问题。

标签: c# concurrency task volatile memory-model


【解决方案1】:

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

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

(我的重点)

所以这被证明是不可预测的(也就是你无法控制的)。

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


我的通灵密码阅读眼睛告诉我,这可能是第一种情况下发生的情况:

  1. 任务已启动,但在调用委托中的实际代码之前会产生开销
  2. 在调用委托之前,主程序继续并设法启动循环,对控制变量进行单次读取,并继续重复使用其缓存副本。
  3. 委托最终会被执行,但这对循环没有影响,因为它已经读取了一次变量并且没有再次执行此操作的意愿。

在第二种情况下,由于第一种情况有效地“通过某些对象引用读取变量”,而第二种情况有效地“通过this 引用读取变量”,上述情况略有改变,这可能施加差异。

但这里真正的答案是你倾向于优化器并且编写了不可预测的代码。

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

对代码进行看似无关的微小更改可以使优化器以不同的方式做事。

【讨论】:

  • @usr 任何一个程序都完全在他们的权利范围内以任何一种方式行事。不保证任何一个程序都会产生任何特定结果。 完全在他们的权利范围内为所欲为的不同程序的结果是不同的,这是完全预期的。
  • @usr 两个程序分别被允许为所欲为而不做完全相同的事情有什么奇怪的?他们必须做完全相同的事情的期望只是一个毫无根据的假设。 这里没有什么奇怪的事情发生
【解决方案2】:

有一篇关于内存模型的文章: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),因为第一个任务还没有完成,这证实了这一点,并且我们的行为与第一个示例中的行为相同。

【讨论】: