Home > Software engineering >  How does Async programming work with Threads when using thread.sleep()?
How does Async programming work with Threads when using thread.sleep()?

Time:05-19

Presumptions/Prelude:

  1. In previous questions, we note that thread.sleep blocks threads see: When to use Task.Delay, when to use Thread.Sleep?.
  2. We also note that console apps have three threads: The main thread, the GC thread & the finalizer thread IIRC. All other threads are debugger threads.
  3. We know that async does not spin up new threads, and it instead runs on the synchronization context, "uses time on the thread only when the method is active". https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/task-asynchronous-programming-model

Setup:
In a sample console app, we can see that neither the sibling nor the parent code are affected by a call to thread.sleep, at least until the await is called (unknown if further).

var sw = new Stopwatch();
sw.Start();
Console.WriteLine($"{sw.Elapsed}");
var asyncTests = new AsyncTests();

var go1 = asyncTests.WriteWithSleep();
var go2 = asyncTests.WriteWithoutSleep();

await go1;
await go2;
sw.Stop();
Console.WriteLine($"{sw.Elapsed}");
        
Stopwatch sw1 = new Stopwatch();
public async Task WriteWithSleep()
{
    sw1.Start();
    await Task.Delay(1000);
    Console.WriteLine("Delayed 1 seconds");
    Console.WriteLine($"{sw1.Elapsed}");
    Thread.Sleep(9000);
    Console.WriteLine("Delayed 10 seconds");
    Console.WriteLine($"{sw1.Elapsed}");
    sw1.Stop();
}
public async Task WriteWithoutSleep()
{
    await Task.Delay(3000);
    Console.WriteLine("Delayed 3 second.");
    Console.WriteLine($"{sw1.Elapsed}");
    await Task.Delay(6000);
    Console.WriteLine("Delayed 9 seconds.");
    Console.WriteLine($"{sw1.Elapsed}");
}

Question: If the thread is blocked from execution during thread.sleep, how is it that it continues to process the parent and sibling? Some answer that it is background threads, but I see no evidence of multithreading background threads. What am I missing?

CodePudding user response:

The Task.Delay method is implemented basically like this (simplified):

public static Task Delay(int millisecondsDelay)
{
    var tcs = new TaskCompletionSource();
    _ = new Timer(_ => tcs.SetResult(), null, millisecondsDelay, -1);
    return tcs.Task;
}

The Task is completed on the callback of a System.Threading.Timer component, and according to the documentation this callback is invoked on a ThreadPool thread:

The method does not execute on the thread that created the timer; it executes on a ThreadPool thread supplied by the system.

So when you await the task returned by the Task.Delay method, the continuation after the await runs on the ThreadPool. The ThreadPool typically has more than one threads available immediately on demand, so it's not difficult to introduce concurrency and parallelism if you create 2 tasks at once, like you do in your example. The main thread of a console application is not equipped with a SynchronizationContext by default, so there is no mechanism in place to prevent the observed concurrency.

CodePudding user response:

I see no evidence of multithreading background threads. What am I missing?

Possibly you are looking in the wrong place, or using the wrong tools. There's a handy property that might be of use to you, in the form of Thread.CurrentThread.ManagedThreadId. According to the docs,

A thread's ManagedThreadId property value serves to uniquely identify that thread within its process.

The value of the ManagedThreadId property does not vary over time

This means that all code running on the same thread will always see the same ManagedThreadId value. If you sprinkle some extra WriteLines into your code, you'll be able to see that your tasks may run on several different threads during their lifetimes. It is even entirely possible for some async applications to have all their tasks run on the same thread, though you probably won't see that behaviour in your code under normal circumstances.

Here's some example output from my machine, not guaranteed to be the same on yours, nor is it necessarily going to be the same output on successive runs of the same application.

00:00:00.0000030
 * WriteWithSleep on thread 1 before await
 * WriteWithoutSleep on thread 1 before first await
 * WriteWithSleep on thread 4 after await
Delayed 1 seconds
00:00:01.0203244
 * WriteWithoutSleep on thread 5 after first await
Delayed 3 second.
00:00:03.0310891
 * WriteWithoutSleep on thread 6 after second await
Delayed 9 seconds.
00:00:09.0609263
Delayed 10 seconds
00:00:10.0257838
00:00:10.0898976

The business of running tasks on threads is handled by a TaskScheduler. You could write one that forces code to be single threaded, but that's not often a useful thing to do. The default scheduler uses a threadpool, and as such tasks can be run on a number of different threads.

  • Related