.Net内存可见性行为

时间:2013-11-23 21:23:38

标签: c# .net multithreading volatile memory-fences

编辑:我最后写了一篇关于这个问题的完整文章: 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 提供最终的汇编代码并分享一些调试智慧。

1 个答案:

答案 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的代码。

x64上的

编辑发布版本结果类似:

            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