Investigating an infinite loop in Release configuration

 
 
  • Gérald Barré

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!

I recently had to investigate an infinite loop in an application. A simplified version of the buggy code is like that:

C#
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:

  1. L004d: movzx ecx, byte ptr [esi+4]: Move the value of the variable isReady in the register ECX
  2. L0051: test ecx, ecx: Check if the value in the ECX register is 0 (false)
  3. L0053: je short L0051: If the value is 0, 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.

C#
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.

C#
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

Do you have a question or a suggestion about this post? Contact me!

Follow me:
Enjoy this blog?Buy Me A Coffee💖 Sponsor on GitHub