Does .NET resume an await continuation on a new different thread pool thread or reuse the thread from a previous resumption?
Let's image below C# code in a .NET Core console application:
using System;
using System.Threading;
using System.Threading.Tasks;
namespace NetCoreResume
{
class Program
{
static async Task AsyncThree()
{
await Task.Run(() =>
{
Console.WriteLine($"AsyncThree Task.Run thread id:{Thread.CurrentThread.ManagedThreadId.ToString()}");
});
Console.WriteLine($"AsyncThree continuation thread id:{Thread.CurrentThread.ManagedThreadId.ToString()}");
}
static async Task AsyncTwo()
{
await AsyncThree();
Console.WriteLine($"AsyncTwo continuation thread id:{Thread.CurrentThread.ManagedThreadId.ToString()}");
}
static async Task AsyncOne()
{
await AsyncTwo();
Console.WriteLine($"AsyncOne continuation thread id:{Thread.CurrentThread.ManagedThreadId.ToString()}");
}
static void Main(string[] args)
{
AsyncOne().Wait();
Console.WriteLine("Press any key to end...");
Console.ReadKey();
}
}
}
It will output:
AsyncThree Task.Run thread id:4
AsyncThree continuation thread id:4
AsyncTwo continuation thread id:4
AsyncOne continuation thread id:4
Press any key to end...
I have tried to add ConfigureAwait(false)
after each await Task
, but it will get the same result.
As we can see, it seems like all await continuations reused the thread created in Task.Run
of AsyncThree()
method.
I want to ask if .NET will always resume the await continuation on previous resumption thread, or it will apply a new different thread from thread pool in some occasions?
I knew there is answer the continuation will resume on a thread pool thread in below discussion:
async/await. Where is continuation of awaitable part of method performed?
Let's exclude the SynchronizationContext
case in above link, since we are now discussing a .NET console application. But I want to ask it seems like that thread pool thread in my example is always thread id 4
, I don't know whether it is because thread id 4
is always free in the thread pool, so every continuation reuse it by coincidence, or .NET has mechanism will reuse the previous resumption thread as much as possible?
Is there any possibility each continuation will resume on a different thread pool thread like below?
AsyncThree Task.Run thread id:4
AsyncThree continuation thread id:5
AsyncTwo continuation thread id:6
AsyncOne continuation thread id:7
Press any key to end...
Does .NET resume an await continuation on a new different thread pool thread or reuse the thread from a previous resumption?
Neither. By default, when await
ing Task
s, await
will capture a "context" and use that to resume the asynchronous method. This "context" is SynchronizationContext.Current
, unless it is null
, in which case the context is TaskScheduler.Current
. In your example code, the context is the thread pool context.
The other part of the puzzle is undocumented: await
uses the TaskContinuationOptions.ExecuteSynchronously
flag. This means that when the Task.Run
task is completed (by thread 4
), its continuations are run immediately and synchronously - if possible. In your example code, the continuation may run synchronously because there's enough stack on thread 4
and the continuation should be run on a thread pool thread and thread 4
is a thread pool thread.
Likewise, when AsyncThree
completes, the continuation for AsyncTwo
is run immediately and synchronously - again on thread 4
since it meets all the criteria.
This is an optimization that is especially helpful in scenarios like ASP.NET, where it's common to have a chain of async
methods and have one task completing (e.g., a db read) that completes the entire chain and sends the response. In those cases you want to avoid unnecessary thread switches.
An interesting side effect of this is that you end up with an "inverted call stack" of sorts: the thread pool thread 4
ran your code and then completed AsyncThree
and then AsyncTwo
and then AsyncOne
, and each of those completions are on the actual call stack. If you place a breakpoint on the WriteLine
in AsyncOne
(and look at external code), you can see where ThreadPoolWorkQueue.Dispatch
(indirectly) called AsyncThree
which (indirectly) called AsyncTwo
which (indirectly) called AsyncOne
.
As you can see Why is the initial thread not used on the code after the awaited method? it is quite possible to resume on another thread, based on what is available at the moment.
In asynchronous programming there is not definite usage of specific threads when used with async await. You only know that an available thread will be picked from the thread pool.
In your case, since the execution is pretty much sequential, the thread is freed and you get the number 4.
Based on the thread pool documentation https://learn.microsoft.com/en-us/dotnet/standard/threading/the-managed-thread-pool the thread pool is unique per process, so I'd expect the usage of the first available thread to be used. So is you have no other concurrent operations, the thread 4 will be reused each time. There are no guarantees though.
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