C# 线程真的可以缓存一个值并忽略其他线程上对该值的更改吗?

Posted

技术标签:

【中文标题】C# 线程真的可以缓存一个值并忽略其他线程上对该值的更改吗?【英文标题】:Can a C# thread really cache a value and ignore changes to that value on other threads? 【发布时间】:2010-10-02 06:31:30 【问题描述】:

这个问题不是关于竞争条件、原子性或为什么你应该在你的代码中使用锁的。我已经知道这些了。

更新:我的问题不是“易失性内存是否存在怪异”(我知道确实存在),我的问题是“.NET 运行时是否抽象了它,所以你永远不会看到它”。

见http://www.yoda.arachsys.com/csharp/threads/volatility.shtml 以及Is a string property itself threadsafe?上的第一个答案

(它们实际上是同一篇文章,因为一篇引用了另一篇。)一个线程设置一个布尔值,另一个线程永远循环读取该布尔值——那些文章声称读取线程可能会缓存旧值而从不读取新值值,因此您需要一个锁(或使用 volatile 关键字)。他们声称以下代码可能会永远循环。 现在我同意锁定变量是一种很好的做法,但我无法相信 .NET 运行时会真正忽略文章声称的内存值变化。我理解他们关于易失性内存与非易失性内存的讨论,并且我同意他们在 非托管 代码中有一个有效的观点,但我不敢相信 .NET 运行时不会正确地抽象它离开,以便下面的代码可以满足您的期望。 这篇文章甚至承认代码“几乎肯定”会起作用(尽管不能保证),所以我打电话给 BS。任何人都可以验证以下代码是否始终有效吗?有没有人能够得到一个失败的案例(也许你不能总是重现它)?

class BackgroundTaskDemo

    private bool stopping = false;

    static void Main()
    
        BackgroundTaskDemo demo = new BackgroundTaskDemo();
        new Thread(demo.DoWork).Start();
        Thread.Sleep(5000);
        demo.stopping = true;
    

    static void DoWork()
    
         while (!stopping)
         
               // Do something here
         
    

【问题讨论】:

我同意这似乎应该始终有效。也许 PEX 可以找到破解它的方法。 那甚至不会编译;如果您将所有内容都设为静态,则该示例可能更接近问题区域(否则,如果 JIT 执行此操作,则遵从可能会停止优化)。 使用工作计数器示例更新;-p 复制成功。必须使用 x86 机器运行时,发布模式,无需调试。对于 x64 架构,这没有经过优化。 【参考方案1】:

重点是:它可能会起作用,但不能保证按照规范工作。人们通常追求的是出于正确原因工作的代码,而不是通过编译器、运行时和 JIT 的侥幸组合来工作,这可能在框架版本、物理 CPU、平台,以及 x86 与 x64 之类的东西。

理解内存模型是一个非常非常复杂的领域,我并不自称是专家;但该领域真正专家的人向我保证,您所看到的行为并不能保证。

您可以发布任意数量的工作示例,但不幸的是,除了“它通常有效”之外,这并不能证明太多。这当然不能证明它保证可以工作。只需要一个反例就可以反驳,但找到它是问题所在......

不,我手头没有。


更新可重复的反例:

using System.Threading;
using System;
static class BackgroundTaskDemo

    // make this volatile to fix it
    private static bool stopping = false;

    static void Main()
    
        new Thread(DoWork).Start();
        Thread.Sleep(5000);
        stopping = true;


        Console.WriteLine("Main exit");
        Console.ReadLine();
    

    static void DoWork()
    
        int i = 0;
        while (!stopping)
        
            i++;
        

        Console.WriteLine("DoWork exit " + i);
    

输出:

Main exit

但仍在运行,CPU 已满;请注意,此时 stopping 已设置为 trueReadLine 是为了使进程不会终止。优化似乎取决于循环内代码的大小(因此i++)。它显然只在“发布”模式下工作。添加volatile,一切正常。

【讨论】:

好答案。我想知道 - 在未来的硬件或当今的硬件上是否不能保证?在我看来,担心某些可能无法在未来的硬件上运行的东西似乎很愚蠢。 澄清一下 - 是否存在编译器、运行时和 JIT 的真实组合可以显示与此代码中断? 是的!已发布;在发布模式等下运行它。 哇。这太疯狂了。因为这个,我的代码中有很多错误!干杯! 复制成功。必须使用 x86 机器运行时,发布模式,无需调试。对于 x64 架构,这没有经过优化。【参考方案2】:

此示例包含作为 cmets 的本机 x86 代码,以演示控制变量 ('stopLooping') 已缓存。

将 'stopLooping' 更改为 volatile 以“修复”它。

这是使用 Visual Studio 2008 作为发布版本构建的,无需调试即可运行。

 using System;

 using System.Threading;

/* A simple console application which demonstrates the need for 
 the volatile keyword and shows the native x86 (JITed) code.*/

static class LoopForeverIfWeLoopOnce


    private static bool stopLooping = false;

    static void Main()
    
        new Thread(Loop).Start();
        Thread.Sleep(1000);
        stopLooping = true;
        Console.Write("Main() is waiting for Enter to be pressed...");
        Console.ReadLine();
        Console.WriteLine("Main() is returning.");
    

    static void Loop()
    
        /*
         * Stack frame setup (Native x86 code):
         *  00000000  push        ebp  
         *  00000001  mov         ebp,esp 
         *  00000003  push        edi  
         *  00000004  push        esi  
         */

        int i = 0;
        /*
         * Initialize 'i' to zero ('i' is in register edi)
         *  00000005  xor         edi,edi 
         */

        while (!stopLooping)
            /*
             * Load 'stopLooping' into eax, test and skip loop if != 0
             *  00000007  movzx       eax,byte ptr ds:[001E2FE0h] 
             *  0000000e  test        eax,eax 
             *  00000010  jne         00000017 
             */
        
            i++;
            /*
             * Increment 'i'
             *  00000012  inc         edi  
             */

            /*
             * Test the cached value of 'stopped' still in
             * register eax and do it again if it's still
             * zero (false), which it is if we get here:
             *  00000013  test        eax,eax 
             *  00000015  je          00000012 
             */
        

        Console.WriteLine("i=0", i);
    

【讨论】:

复制成功。必须使用 x86 机器运行时,发布模式,无需调试。对于 x64 架构,这没有经过优化。【参考方案3】:

FWIW:

已经从 MS C++ 编译器(非托管代码)中看到了这种编译器优化。 我不知道C#中是否会发生这种情况 调试时不会发生(调试时会自动禁用编译器优化) 即使现在没有进行这种优化,您也可以打赌他们永远不会在 JIT 编译器的未来版本中引入这种优化。

【讨论】:

以上是关于C# 线程真的可以缓存一个值并忽略其他线程上对该值的更改吗?的主要内容,如果未能解决你的问题,请参考以下文章

关闭后忽略对coroutine通道的提议

c# 线程池实现 只是一个原理性的实现细节内容忽略

Synchronizedvolatile与锁

volatile关键字 - 内存可见性

C#原子操作(Interlocked.Decrement和Interlocked.Increment)

php从数据库中删除值并使其他不变