Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Does .NET resume an await continuation on a new different thread pool thread or reuse the thread from a previous resumption?

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...
like image 835
Scott.Hu Avatar asked Jan 08 '20 16:01

Scott.Hu


2 Answers

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 awaiting Tasks, 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.

like image 108
Stephen Cleary Avatar answered Nov 02 '22 02:11

Stephen Cleary


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.

like image 33
Athanasios Kataras Avatar answered Nov 02 '22 04:11

Athanasios Kataras