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
已设置为 true
。 ReadLine
是为了使进程不会终止。优化似乎取决于循环内代码的大小(因此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# 线程真的可以缓存一个值并忽略其他线程上对该值的更改吗?的主要内容,如果未能解决你的问题,请参考以下文章