I recommend you read my async
intro post for an understanding of the async
and await
keywords. In particular, await
(by default) will capture a “context” and use that context to resume its asynchronous method. This “context” is the current SynchronizationContext
(or TaskScheduler
, if there is no SynchronzationContext
).
I want to know where does the asynchronously part run, if there are no other threads created? If it runs on the same thread, shouldn’t it block it due to long I/O request, or compiler is smart enough to move that action to another thread if it takes too long, and a new thread is used after all?
As I explain on my blog, truly asynchronous operations do not “run” anywhere. In this particular case (Task.Delay(1)
), the asynchronous operation is based off a timer, not a thread blocked somewhere doing a Thread.Sleep
. Most I/O is done the same way. HttpClient.GetAsync
for example, is based off overlapped (asynchronous) I/O, not a thread blocked somewhere waiting for the HTTP download to complete.
Once you understand how await
uses its context, walking through the original code is easier:
static void Main(string[] args)
{
Console.WriteLine("Main: " + Thread.CurrentThread.ManagedThreadId);
MainAsync(args).Wait(); // Note: This is the same as "var task = MainAsync(args); task.Wait();"
Console.WriteLine("Main End: " + Thread.CurrentThread.ManagedThreadId);
Console.ReadKey();
}
static async Task MainAsync(string[] args)
{
Console.WriteLine("Main Async: " + Thread.CurrentThread.ManagedThreadId);
await thisIsAsync(); // Note: This is the same as "var task = thisIsAsync(); await task;"
}
private static async Task thisIsAsync()
{
Console.WriteLine("thisIsAsyncStart: " + Thread.CurrentThread.ManagedThreadId);
await Task.Delay(1); // Note: This is the same as "var task = Task.Delay(1); await task;"
Console.WriteLine("thisIsAsyncEnd: " + Thread.CurrentThread.ManagedThreadId);
}
- The main thread starts executing
Main
and callsMainAsync
. - The main thread is executing
MainAsync
and callsthisIsAsync
. - The main thread is executing
thisIsAsync
and callsTask.Delay
. Task.Delay
does its thing – starting a timer and whatnot – and returns an incomplete task (note thatTask.Delay(0)
would return a completed task, which alters the behavior).- The main thread returns to
thisIsAsync
and awaits the task returned fromTask.Delay
. Since the task is incomplete, it returns an incomplete task fromthisIsAsync
. - The main thread returns to
MainAsync
and awaits the task returned fromthisIsAsync
. Since the task is incomplete, it returns an incomplete task fromMainAsync
. - The main thread returns to
Main
and callsWait
on the task returned fromMainAsync
. This will block the main thread untilMainAsync
completes. - When the timer set by
Task.Delay
goes off,thisIsAsync
will continue executing. Since there is noSynchronizationContext
orTaskScheduler
captured by thatawait
, it resumes executing on a thread pool thread. - The thread pool thread reaches the end of
thisIsAsync
, which completes its task. MainAsync
continues executing. Since there is no context captured by thatawait
, it resumes executing on a thread pool thread (actually the same thread that was runningthisIsAsync
).- The thread pool thread reaches the end of
MainAsync
, which completes its task. - The main thread returns from its call to
Wait
and continues executing theMain
method. The thread pool thread used to continuethisIsAsync
andMainAsync
is no longer needed and returns to the thread pool.
The important takeaway here is that the thread pool is used because there’s no context. It is not automagically used “when necessary”. If you were to run the same MainAsync
/thisIsAsync
code inside a GUI application, then you would see very different thread usage: UI threads have a SynchronizationContext
that schedules continuations back onto the UI thread, so all the methods will resume on that same UI thread.