I was reading about async/await recently and I am confused with the fact that many of the articles/posts I was reading state that new thread is not created when using async await (Example).
I have created a simple console app to test it
class Program { static void Main(string[] args) { Console.WriteLine("Main: " + Thread.CurrentThread.ManagedThreadId); MainAsync(args).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(); } private static async Task thisIsAsync() { Console.WriteLine("thisIsAsyncStart: " + Thread.CurrentThread.ManagedThreadId); await Task.Delay(1); Console.WriteLine("thisIsAsyncEnd: " + Thread.CurrentThread.ManagedThreadId); } }
Output of the following code is:
Main: 8 Main Async: 8 thisIsAsyncStart: 8 thisIsAsyncEnd: 9 Main End: 8
Am I missing the point, or thisIsAsyncEnd is having different thread ID than other actions?
EDIT:
I have updated code as suggested in the answer below to await Task.Delay(1)
, but I am still seeing the same results.
Quote from the answer below:
Rather, it enables the method to be split into multiple pieces, some of which may run asynchronously
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?
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); }
Main
and calls MainAsync
.MainAsync
and calls thisIsAsync
.thisIsAsync
and calls Task.Delay
.Task.Delay
does its thing - starting a timer and whatnot - and returns an incomplete task (note that Task.Delay(0)
would return a completed task, which alters the behavior).thisIsAsync
and awaits the task returned from Task.Delay
. Since the task is incomplete, it returns an incomplete task from thisIsAsync
.MainAsync
and awaits the task returned from thisIsAsync
. Since the task is incomplete, it returns an incomplete task from MainAsync
.Main
and calls Wait
on the task returned from MainAsync
. This will block the main thread until MainAsync
completes.Task.Delay
goes off, thisIsAsync
will continue executing. Since there is no SynchronizationContext
or TaskScheduler
captured by that await
, it resumes executing on a thread pool thread.thisIsAsync
, which completes its task.MainAsync
continues executing. Since there is no context captured by that await
, it resumes executing on a thread pool thread (actually the same thread that was running thisIsAsync
).MainAsync
, which completes its task.Wait
and continues executing the Main
method. The thread pool thread used to continue thisIsAsync
and MainAsync
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With