문제

EDIT: I've finally written a complete article about the issue: Synchronization, memory visibility and leaky abstractions


I'm demonstrating the importance of volatile read with this code:

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.");
}

As expected the thread is not aware of the new ok value and the program hangs.

But to obtain this behavior I have to do something in the while loop, e.g. incrementing an integer.

If I remove the ++n statement, the thread reads the new value and exits.

I guess it has something to do with the JITter optimizations because as far as CIL is concerned there is nothing (at least for a layman like me):

.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
}

And, on the contrary, I would naively expect that doing something in the loop would increase the odds for the thread to trigger a cache refresh.

What am I missing again?


FINAL EDIT: this is again some JITter black-magic.

Kudos to Hans for confirming this is a "well-known" JITter "issue" and for pointing out that in x64 we get the "expected" behavior.

Kudos to MagnatLU for providing the resulting assembly code and for sharing some debugging wisdom.

도움이 되었습니까?

해결책

As you wrote, it's all in the JITter. In Release build and without debugger attached, with ++n you get:

            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 

And without ++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 

The real question should be why there is no code for ++n emitted at all.

Edit: on x64 Release build results are similar:

            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 
라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 StackOverflow
scroll top