Investigating an infinite loop in Release configuration
This post is part of the series 'Crash investigations and code reviews'. Be sure to check out the rest of the blog posts of the series!
- Investigating a performance issue with a regex
- Investigating an infinite loop in Release configuration (this post)
- Investigating a crash in Enumerable.LastOrDefault with a custom collection
I recently had to investigate an infinite loop in an application. A simplified version of the buggy code is like that:
static void Main()
{
bool isReady = false;
var thread = new Thread(_ =>
{
// ... (initialization)
isReady = true;
// ... (code after initialization)
});
thread.Start();
// wait for the other thread to do some initialization
while (!isReady)
{
// code omitted for brevity
}
Console.WriteLine("Hello World!");
}
The code looks valid, maybe not the best C# code, but valid. The main thread starts another thread to do some initialization work in the background. Once the initialization is done, it continue its execution. The developer tests it on its machine, and everything's ok.
After publishing the application and starting it, the program is stuck. The while
loop never completes. After investigation, this code doesn't work in Release configuration.
The Release configuration allows more optimizations. So, this means you need to look at the generated assembler to understand what happens at runtime.
Source: SharpLab
The interesting part is the loop:
L004d: movzx ecx, byte ptr [esi+4]
: Move the value of the variableisReady
in the register ECXL0051: test ecx, ecx
: Check if the value in the ECX register is 0 (false)L0053: je short L0051
: If the value is0
, go to step 2
The value is read once before the loop. Then, the loop starts and always checks the same value. So, the value set by the other thread is not read. The JIT is doing this optimization because the method doesn't assign the variable. Indeed the method in the Thread.Start
constructor is another method in the generated code.
Now that we have found why the program stays in an infinite loop, let's check the possible fixes.
#Fix #1: Volatile.Read
A possible fix is to use a Volatile.Read
(documentation). This method returns the latest value written by any processor in the computer, regardless of the number of processors or the state of the processor cache. This ensures the generated assembly always reads the value from the memory when reading it.
while (!Volatile.Read(ref isReady))
{
}
; https://sharplab.io/#v2:C4LghgzgtgPgAgJgIwFgBQcAMACOSB0AKgBYBOApmACYCWAdgOYDc66iuSA7OgN7rYCOANlwAWbAFkw9ABR5MAbQC62MKQYQAlP0F80gg9gBGAexMAbbDQgAlSlQCe2ALzYAZmHMRyLNDsPYAG5q2MBk9i7YdOQA7tgkFNQyAPouAHxWtvZOrsCkAK7kmr4BAmGJVPgAysBqwDLFrPqlMcQ05uTYMgCEAGoWYMDt5Ph2SRRumWOOmtrNAXql2AC+TUt4AJwyAEQAEuTm5ibYAOompOZU3duN8yvoq2hAA===
L0050: mov ecx, esi
L0052: cmp byte ptr [ecx], 0 ; Read value from the memory and compare it with 0
L0055: je short L0050
#Fix #2: Synchronization primitives
Another fix, which I prefer, is to use synchronization primitives to wake up the main thread when the other thread has completed its code. For instance, you can use a ManualResetEventSlim to block the thread until the other thread is ready.
static void Main()
{
var resetEvent = new ManualResetEventSlim(false);
var thread = new Thread(_ =>
{
// ...
resetEvent.Set();
// ...
});
thread.Start();
// wait for the other thread for doing the job
resetEvent.Wait();
Console.WriteLine("Hello World!");
}
#Additional resources
- C# - The C# Memory Model in Theory and Practice
- C# - The C# Memory Model in Theory and Practice, Part 2
- Weak vs. Strong Memory Models
Do you have a question or a suggestion about this post? Contact me!