Thread.Sleep(0) not working as expected on .NET Core 3.1

Viewed 246

So, i'm studying the .NET Thread class based on the book: Exam Ref 70-483: Programming in C#, and i noticed something i don't know why it happens or if it should be happening.

The book give me this part of code on page 4:

public static class Program
{
   public static void ThreadMethod()
   {
       for (int i = 0; i < 10; i++)
       {
           Console.WriteLine("ThreadProc: {0}", i);
           Thread.Sleep(0);
       }
   }
   public static void Main()
   {
       Thread t = new Thread(new ThreadStart(ThreadMethod));
       t.Start();
       for (int i = 0; i < 4; i++)
       {
           Console.WriteLine("Main thread: Do some work.");
           Thread.Sleep(0);
       }
       t.Join();
   }
}

With the expected result showing below, since at each Thread.Sleep(0), the current thread exits and go to work on the next thread on the queue:

// Main thread: Do some work.  
// ThreadProc: 0  
// Main thread: Do some work.  
// ThreadProc: 1  
// Main thread: Do some work.  
// ThreadProc: 2  
// Main thread: Do some work.  
// ThreadProc: 3  
// ThreadProc: 4  
// ThreadProc: 5  
// ThreadProc: 6  
// ThreadProc: 7  
// ThreadProc: 8  
// ThreadProc: 9  

But when i built my version of this, it didn't work as expected, even when i copy pasted the code and ran on my machine, it give this result every time a executed the program, with the same code above:

// Main thread: Do some work.  
// ThreadProc: 0  
// ThreadProc: 1  
// ThreadProc: 2  
// ThreadProc: 3  
// ThreadProc: 4  
// ThreadProc: 5  
// ThreadProc: 6  
// ThreadProc: 7  
// ThreadProc: 8  
// ThreadProc: 9  
// Main thread: Do some work.  
// Main thread: Do some work.  
// Main thread: Do some work.  

As you can see, it seems to enter the side thread and when i call Thread.Sleep(0) method it don't go back to the main thread as it should, and keeps working on the side thread until it finishes.

As an workaround for this, i used the Random class to generate a random number between 100 and 500 to pass to the Thread.Sleep() method, and it starts to work as expected:

public static class Program
{
    public static void ThreadMethod()
    {
        var rand = new Random();
        for (int i = 0; i < 10; i++)
        {
            Console.WriteLine("ThreadProc: {0}", i);
            Thread.Sleep(rand.Next(100, 500));
        }
    }
    public static void Main()
    {
        Thread t = new Thread(new ThreadStart(ThreadMethod));
        t.Start();
        var rand = new Random();
        for (int i = 0; i < 4; i++)
        {
            Console.WriteLine("Main thread: Do some work.");
            Thread.Sleep(rand.Next(100, 500));
        }
        t.Join();
    }
}

With the results looking like this:

// Main thread: Do some work.  
// ThreadProc: 0  
// ThreadProc: 1  
// ThreadProc: 2  
// Main thread: Do some work.  
// ThreadProc: 3  
// Main thread: Do some work.  
// ThreadProc: 4  
// Main thread: Do some work.  
// ThreadProc: 5  
// ThreadProc: 6  
// ThreadProc: 7  
// ThreadProc: 8  
// ThreadProc: 9  

I don't think the book was written on the .NET Core framework, and i thought this "problem" may be caused by the changes on the synchronization context between these two frameworks. I checked the priority of the two threads and is set as the default "Normal".

Anyone have an idea of why this happens?

2 Answers

You won't see any difference of behavior between .net framework and .net core for this example.

The book was written at an era where most consumer CPUs were single core. With a single core CPU, only one thread can be scheduled at a time, and therefore the output of the sample is predictable.

Nowadays, pretty much all CPUs have multiple cores, meaning that multiple threads can be scheduled at the same time. This makes the output of the program unpredictable, and unaffected by the calls to Thread.Sleep(0).

One way to get the behavior described on the book on modern hardware is to set the affinity of your process so that it runs on a single core:

> start /affinity 1 CoreConsoleApp1.exe

By doing that, I get the expected interleaved output:

ThreadProc: 0
Main thread: Do some work.
ThreadProc: 1
Main thread: Do some work.
ThreadProc: 2
Main thread: Do some work.
ThreadProc: 3
Main thread: Do some work.
ThreadProc: 4
ThreadProc: 5
ThreadProc: 6
ThreadProc: 7
ThreadProc: 8
ThreadProc: 9

Don't expect definite behavior with threads. If you are going for synchronization, don't use the sleep function.

Try to change the main loop to 10000 loops and do the same with the thread loop.

You'll see a much better and expected result. The fact that it can give the remaining time slice to another thread, does not mean that it won't be the new thread you are creating (instead of the main one)

That is, the .net framework decides the optimal way to schedule thread time and there are no guarantees. The Thread.Sleep function is there to just make sure that the other threads will sometime in the future be responsive.

Related