async/await different thread ID
Solution 1
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.
Solution 2
I wondered exactly the same. For me the explanations by MSDN were contradictory:
The async and await keywords don't cause additional threads to be created. Async methods don't require multithreading because an async method doesn't run on its own thread.
MSDN: Asynchronous Programming with async and await
An await expression does not block the thread on which it is executing. [..] When the task completes, it invokes its continuation, and execution of the async method resumes where it left off.
I didn't understand how the original thread could be not blocked, without the use of additional threads. Additionally the "invoke" wording suggested that there are multiple threads used somewhere and somehow.
But then I realized, that everything is written correctly, that there aren't used any other threads by these keywords. It is by design of the Task
class to provide mechanisms that may use different threads - or not.
While stephen-cleary beautifully explained these mechanisms for the Task.Delay()
method, I extended the MSDN example to learn how await
behaves with Task.Run()
:
private async void ds_StartButton_Click(object sender, EventArgs e)
{
textBox1.AppendText(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Started MSDN Example ..." + Environment.NewLine);
// Call the method that runs asynchronously.
string result = await WaitAsynchronouslyAsync();
// Call the method that runs synchronously.
//string result = await WaitSynchronously ();
// Do other Schdaff
textBox1.AppendText(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Foobar #1 ..." + Environment.NewLine);
textBox1.AppendText(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Foobar #2 ..." + Environment.NewLine);
textBox1.AppendText(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Foobar #3 ..." + Environment.NewLine);
textBox1.AppendText(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Foobar #4 ..." + Environment.NewLine);
textBox1.AppendText(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Foobar #5 ..." + Environment.NewLine);
textBox1.AppendText(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Foobar #6 ..." + Environment.NewLine);
textBox1.AppendText(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Foobar #7 ..." + Environment.NewLine);
// Display the result.
textBox1.Text += result;
}
// The following method runs asynchronously. The UI thread is not
// blocked during the delay. You can move or resize the Form1 window
// while Task.Delay is running.
public async Task<string> WaitAsynchronouslyAsync()
{
Console.WriteLine(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Entered WaitAsynchronouslyAsync()");
await Task.Delay(10000);
Console.WriteLine(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Task.Delay done, starting random string generation now ...");
await Task.Run(() => LongComputation());
Console.WriteLine(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Leaving WaitAsynchronouslyAsync() ...");
return DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Finished MSDN Example." + Environment.NewLine;
}
// The following method runs synchronously, despite the use of async.
// You cannot move or resize the Form1 window while Thread.Sleep
// is running because the UI thread is blocked.
public async Task<string> WaitSynchronously()
{
// Add a using directive for System.Threading.
Thread.Sleep(10000);
return DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Finished MSDN Bad Ass Example." + Environment.NewLine;
}
private void ds_ButtonTest_Click(object sender, EventArgs e)
{
textBox1.AppendText(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Started Test ..." + Environment.NewLine);
Task<string> l_Task = WaitAsynchronouslyAsync();
//WaitAsynchronouslyAsync();
//textBox1.AppendText(l_Result);
}
private void LongComputation()
{
Console.WriteLine(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Generating random string ...");
string l_RandomString = GetRandomString(10000000);
Console.WriteLine(DateTime.Now.ToString() + " [" + Thread.CurrentThread.ManagedThreadId + "] Random string generated.");
}
/// <summary>Get random string with specified length</summary>
/// <param name="p_Length">Requested length of random string</param>
/// <param name="p_NoDots">Use case of this is unknown, but assumed to be importantly needed somewhere. Defaults to true therefore.
/// But due to huge performance implication, added this parameter to switch this off.</param>
/// <returns>Random string</returns>
public static string GetRandomString(int p_Length, bool p_NoDots = true)
{
StringBuilder l_StringBuilder = new StringBuilder();
string l_RandomString = string.Empty;
while (l_StringBuilder.Length <= p_Length)
{
l_RandomString = (p_NoDots ? System.IO.Path.GetRandomFileName().Replace(".", string.Empty) : System.IO.Path.GetRandomFileName());
l_StringBuilder.Append(l_RandomString);
}
l_RandomString = l_StringBuilder.ToString(0, p_Length);
l_StringBuilder = null;
return l_RandomString;
}
As you can see from the output, there are multiple threads used - not by async/await
, but by Task.Run()
:
04.11.2016 12:38:06 [10] Entered WaitAsynchronouslyAsync()
04.11.2016 12:38:17 [10] Task.Delay done, starting random string generation now ...
04.11.2016 12:38:17 [12] Generating random string ...
04.11.2016 12:38:21 [12] Random string generated.
04.11.2016 12:38:21 [10] Leaving WaitAsynchronouslyAsync() ...
This is as usual as it ever have been, but me personally needed this explicit example to understand what's going on and to separate what's done by async/await
and what's done by Task
.
Solution 3
Very good explanation for your question is here https://blogs.msdn.microsoft.com/pfxteam/2012/01/20/await-synchronizationcontext-and-console-apps/
When your console application’s Main method is invoked, SynchronizationContext.Current will return null. That means that if you invoke an asynchronous method in your console app, unless you do something special, your asynchronous methods will not have thread affinity: the continuations within those asynchronous methods could end up running “anywhere.”
Solution 4
Making the method with async
don't mean that it will create another thread.If the RunTime is seeing that your method which is called with await
in your async
method is delayed, it is exiting from that method and waits after awaited methods finish and then continue that method with another thread.Try to change your Task.Delay(2000)
to Task.Delay(0)
and you will see that it doesn't create a new Thread.
The RunTime will count it, if it needs to create it will create if not - not.I tried your example with 0 ms and get all the same thread:
Main: 1
Main Async: 1
thisIsAsyncStart: 1
thisIsAsyncEnd: 1
Main End: 1
Taken from Stephen Toub's blog:
The “Async” Keyword
What does the “async” keyword do when applied to a method?
When you mark a method with the “async” keyword, you’re really telling the compiler two things:
- You’re telling the compiler that you want to be able to use the “await” keyword inside the method (you can use the await keyword if and only if the method or lambda it’s in is marked as async). In doing so, you’re telling the compiler to compile the method using a state machine, such that the method will be able to suspend and then resume asynchronously at await points.
- You’re telling the compiler to “lift” the result of the method or any exceptions that may occur into the return type. For a method that returns Task or Task, this means that any returned value or exception that goes unhandled within the method is stored into the result task. For a method that returns void, this means that any exceptions are propagated to the caller’s context via whatever “SynchronizationContext” was current at the time of the method’s initial invocation.
Does using the “async” keyword on a method force all invocations of that method to be asynchronous?
No. When you invoke a method marked as “async”, it begins running synchronously on the curren thread. So, if you have a synchronous method that returns void and all you do to change it is mark it as “async”, invocations of that method will still run synchronously. This is true regardless of whether you leave the return type as “void” or change it to “Task”. Similarly, if you have a synchronous method that returns some TResult, and all you do is mark it as “async” and change the return type to be “Task”, invocations of that method will still run synchronously.
Marking a method as “async” does not affect whether the method runs to completion synchronously or asynchronously. Rather, it enables the method to be split into multiple pieces, some of which may run asynchronously, such that the method may complete asynchronously. The boundaries of these pieces can occur only where you explicitly code one using the “await” keyword, so if “await” isn’t used at all in a method’s code, there will only be one piece, and since that piece will start running synchronously, it (and the whole method with it) will complete synchronously.
Robert
I am a .NET developer, focused mainly on a web development. I know a bit of PHP, but my point of interest are .NET and Web technologies.
Updated on June 16, 2022Comments
-
Robert almost 2 years
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? -
Willem van Rumpt over 8 yearsIt's common courtesy to indicate that: a) You're quoting someone, b) Who you're quoting, and c) The source of the quote
-
Robert over 8 yearsI have changed code to use
Task.Delay(1)
and i still got the same result in output. -
Robert over 8 years@SurenSrapyan With
Task.Delay(0)
all threads share the same ID, but this does not explain my question. Is the new thread used after all in cases such as this, and async await actually creates a new thread by evaluating needs of this particular case? -
Willem van Rumpt over 8 yearsNothing wrong with quoting, it's just quite normal to attribute the quote to the one who wrote it. You're not quoting documentation, you're quoting an article. It would seem other people agree with me, to the point of editing the post so it actually does.
-
Robert over 8 years@SurenSrapyan that was my question. So it does use new thread after all.
-
Robert over 8 yearsplease update your answer and i will mark it as correct
-
Robert over 8 years@SurenSrapyan I understand it perfectly, i was just confused cause this post stackoverflow.com/questions/27265818/… states it does not create a new thread.
-
Robert over 8 yearsThanks, I will change this to a correct answer because it has much more details, and it is making a previous answer kinda incorrect.
-
Frederik almost 8 yearsAwesome answer. Very much appreciated.
-
Stephen Cleary over 7 yearsGood example. But bear in mind that there are two kinds of tasks: Delegate Tasks and Promise Tasks. Only Delegate Tasks actually execute code (and thus have a thread that they run on). These are
StartNew
and (somewhat)Task.Run
. The vast majority of tasks used withawait
are Promise Tasks, which have no code (and no thread). These are I/O operations,Task.Delay
,async Task
, etc. So, it' true thatTask.Run
uses the thread pool, but it's also true thatTask.Run
is not representative of most async code. -
Nicolas over 7 yearsThanks for the clarification. I am looking forward to encountering the first use cases in my real implementations.
-
h-rai about 7 years@StephenCleary So when the execution reaches 'Main' it is awaiting (or the execution pauses). Is it when the continuation in thisIsAsync (after the execution pauses in the main thread) gets picked up by a thread in the thread pool and executes it? Also, how does the execution know to start the continuation in a different thread from thread pool (or main thread in UI app)?
-
Stephen Cleary about 7 years@nick-s: I explain this in my async intro post:
await
will capture a "context", and use thread pool threads if there is no "context". UI threads provide a context that directsawait
to resume on the UI thread by default. -
Csharp Guy about 4 yearsI get the Thread as "1" even on a WPF application. What am i missing?
-
Stephen Cleary about 4 years@CsharpGuy: If you're referring to
thisIsAsyncEnd
, then the most likely reason is that the delay task is completed by the time it is awaited. In that case,await
will continue executing synchronously. To see it switch threads, increase the delay.