Domanda

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.

È stato utile?

Soluzione

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 
Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top