编辑:我最后写了一篇关于这个问题的完整文章: Synchronization, memory visibility and leaky abstractions
我正在使用此代码演示易失性读取的重要性:
bool ok = false;
void F()
{
int n = 0;
while (!ok) ++n;
}
public void Run()
{
Thread thread = new Thread(F);
thread.Start();
Console.Write("Press enter to notify thread...");
Console.ReadLine();
ok = true;
Console.WriteLine("Thread notified.");
}
正如预期的那样,线程不知道新的ok
值并且程序挂起。
但为了获得这种行为,我必须在while
循环中执行某些操作,例如递增一个整数。
如果删除++n
语句,则线程会读取新值并退出。
我想这与 JITter优化有关,因为就CIL而言,没有任何东西(至少对于像我这样的外行人):
.method private hidebysig instance void F() cil managed
{
.maxstack 2
.locals init ([0] int32 n)
IL_0000: ldc.i4.0
IL_0001: stloc.0
IL_0002: br.s IL_0008
IL_0004: ldloc.0
IL_0005: ldc.i4.1
IL_0006: add
IL_0007: stloc.0
IL_0008: ldarg.0
IL_0009: ldfld bool ThreadingSamples.MemoryVisibilitySample::ok
IL_000e: brfalse.s IL_0004
IL_0010: ret
}
.method private hidebysig instance void F() cil managed
{
.maxstack 8
IL_0000: ldarg.0
IL_0001: ldfld bool ThreadingSamples.MemoryVisibilitySample::ok
IL_0006: brfalse.s IL_0000
IL_0008: ret
}
相反,我会天真地期望在循环中做某事会增加线程触发缓存刷新的几率。
我又错过了什么?
最终编辑:这又是一些 JITter 黑魔法。
感谢Hans确认这是一个“众所周知的”JITter“问题”,并指出在 x64 中我们得到了“预期”行为。
感谢 MagnatLU 提供最终的汇编代码并分享一些调试智慧。
答案 0 :(得分:3)
正如你所写,这一切都在JITter中。在发布版本中,如果没有附带调试器,则会得到++n
:
int n = 0;
00000000 push ebp
00000001 mov ebp,esp
while (!ok) ++n;
00000003 movzx eax,byte ptr [ecx+4]
00000007 test eax,eax
00000009 jne 0000000F
0000000b test eax,eax ; <---
0000000d je 0000000B ; <---
0000000f pop ebp
}
00000010 ret
没有++n
:
while (!ok) ;
00000000 push ebp
00000001 mov ebp,esp
00000003 cmp byte ptr [ecx+4],0
00000007 je 00000003
00000009 pop ebp
}
0000000a ret
真正的问题应该是为什么根本没有发出++n
的代码。
编辑发布版本结果类似:
Debugger.Break();
00000000 push rbx
00000001 sub rsp,20h
00000005 mov rbx,rcx
00000008 call FFFFFFFFED0EE4D0
0000000d mov ecx,2710h
00000012 call FFFFFFFFEDCFE460
while (!ok) ++n;
00000017 mov al,byte ptr [rbx+8]
0000001a movzx ecx,al
0000001d test ecx,ecx
0000001f jne 0000000000000025
00000021 test ecx,ecx
00000023 je 0000000000000021
00000025 add rsp,20h
00000029 pop rbx
0000002a rep ret