In the previous post, I asked why the following code behaves differently when compilation is made in Release and Debug mode.
class Program { static void Main(string[] args) { var w = new Worker(); while (!w.IsDone); Console.WriteLine("The work is done."); } } class Worker { public bool IsDone; public Worker() { var thread = new Thread(Job); thread.Start(); } private void Job() { Thread.Sleep(3000); IsDone = true; } }
Let’s check the assembly code generated by the JIT in these two situations.
The following assembly code is what we get when compiling in Debug mode.
{ 00007FF9AE2E14A2 push rsi 00007FF9AE2E14A3 sub rsp,40h 00007FF9AE2E14A7 mov rbp,rsp 00007FF9AE2E14AA mov rsi,rcx 00007FF9AE2E14AD lea rdi,[rbp+20h] 00007FF9AE2E14B1 mov ecx,8 00007FF9AE2E14B6 xor eax,eax 00007FF9AE2E14B8 rep stos dword ptr [rdi] 00007FF9AE2E14BA mov rcx,rsi 00007FF9AE2E14BD mov qword ptr [rbp+60h],rcx 00007FF9AE2E14C1 cmp dword ptr [7FF9AE1C49D8h],0 00007FF9AE2E14C8 je 00007FF9AE2E14CF 00007FF9AE2E14CA call 00007FFA0DF1FDA0 00007FF9AE2E14CF nop var w = new Worker(); 00007FF9AE2E14D0 mov rcx,7FF9AE1C55F8h 00007FF9AE2E14DA call 00007FFA0DDB00F0 00007FF9AE2E14DF mov qword ptr [rbp+20h],rax 00007FF9AE2E14E3 mov rcx,qword ptr [rbp+20h] 00007FF9AE2E14E7 call 00007FF9AE2E10B0 00007FF9AE2E14EC mov rcx,qword ptr [rbp+20h] 00007FF9AE2E14F0 mov qword ptr [rbp+30h],rcx 00007FF9AE2E14F4 nop 00007FF9AE2E14F5 jmp 00007FF9AE2E14F8 while (!w.IsDone); 00007FF9AE2E14F7 nop 00007FF9AE2E14F8 mov rcx,qword ptr [rbp+30h] 00007FF9AE2E14FC movzx ecx,byte ptr [rcx+8] 00007FF9AE2E1500 test ecx,ecx 00007FF9AE2E1502 sete cl 00007FF9AE2E1505 movzx ecx,cl 00007FF9AE2E1508 mov dword ptr [rbp+2Ch],ecx 00007FF9AE2E150B cmp dword ptr [rbp+2Ch],0 00007FF9AE2E150F jne 00007FF9AE2E14F7 Console.WriteLine("The work is done."); 00007FF9AE2E1511 mov rcx,236EBF53068h 00007FF9AE2E151B mov rcx,qword ptr [rcx] 00007FF9AE2E151E call 00007FF9AE2E1368 00007FF9AE2E1523 nop } 00007FF9AE2E1524 nop 00007FF9AE2E1525 lea rsp,[rbp+40h] 00007FF9AE2E1529 pop rsi 00007FF9AE2E152A pop rdi 00007FF9AE2E152B pop rbp 00007FF9AE2E152C ret
And the following assembly code is what we get when compiling in Release mode.
var w = new Worker(); 00007FF9AE2E14A2 sub esp,20h 00007FF9AE2E14A5 mov rcx,7FF9AE1C55C8h 00007FF9AE2E14AF call 00007FFA0DDB00F0 00007FF9AE2E14B4 mov rsi,rax 00007FF9AE2E14B7 mov rcx,rsi 00007FF9AE2E14BA call 00007FF9AE2E10B0 00007FF9AE2E14BF movzx ecx,byte ptr [rsi+8] while (!w.IsDone); 00007FF9AE2E14C3 test ecx,ecx 00007FF9AE2E14C5 je 00007FF9AE2E14C3 Console.WriteLine("The work is done."); 00007FF9AE2E14C7 mov rcx,16EC2D73068h 00007FF9AE2E14D1 mov rcx,qword ptr [rcx] 00007FF9AE2E14D4 call 00007FF9AE2E1368 00007FF9AE2E14D9 nop 00007FF9AE2E14DA add rsp,20h 00007FF9AE2E14DE pop rsi 00007FF9AE2E14DF ret
As you can see, the JIT does not update the register. That is is perfect for single-threaded applications. But, it does not work with multi-threading.
The solution is to mark the variable as volatile, forcing the runtime to get the updated value all the time.
var w = new Worker(); 00007FF9AE2D14A2 sub esp,20h 00007FF9AE2D14A5 mov rcx,7FF9AE1B55C8h 00007FF9AE2D14AF call 00007FFA0DDB00F0 00007FF9AE2D14B4 mov rsi,rax 00007FF9AE2D14B7 mov rcx,rsi 00007FF9AE2D14BA call 00007FF9AE2D10B0 while (!w.IsDone); 00007FF9AE2D14BF cmp byte ptr [rsi+8],0 00007FF9AE2D14C3 je 00007FF9AE2D14BF Console.WriteLine("The work is done."); 00007FF9AE2D14C5 mov rcx,2772C0D3068h 00007FF9AE2D14CF mov rcx,qword ptr [rcx] 00007FF9AE2D14D2 call 00007FF9AE2D1368 00007FF9AE2D14D7 nop 00007FF9AE2D14D8 add rsp,20h 00007FF9AE2D14DC pop rsi 00007FF9AE2D14DD ret