0

Lets assume I have the following simple program which uses the await operator in both DownloadDocsMainPageAsync() and Main(). While I understand that the current awaitable method gets suspended and continue from that point after the results are available, I need some clarity on the following points .

a) If the execution from Main() starts on Thread A from the threadpool , as soon as it encounters the await operator will this thread be returned to the threadpool for executing other operations in the program , for eg: if its a web app then for invocation of some Controller methods after button clicks from UI?

b) Will the await operator always take the execution on a new thread from the threadpool or in this case assuming there is no other method to be executed apart from Main() ,will it continue execution on the same thread itself (ThreadA)? If my understanding is correct who decides this , is it Garbage collector of CLR?

using System;
using System.Net.Http;
using System.Threading.Tasks;

public class AwaitOperator
{
    public static async Task Main()
    {
        Task<int> downloading = DownloadDocsMainPageAsync();
        Console.WriteLine($"{nameof(Main)}: Launched downloading.");

        int bytesLoaded = await downloading;
        Console.WriteLine($"{nameof(Main)}: Downloaded {bytesLoaded} bytes.");
    }

    private static async Task<int> DownloadDocsMainPageAsync()
    {
        Console.WriteLine($"{nameof(DownloadDocsMainPageAsync)}: About to start downloading.");

        var client = new HttpClient();
        byte[] content = await client.GetByteArrayAsync("https://learn.microsoft.com/en-us/");

        Console.WriteLine($"{nameof(DownloadDocsMainPageAsync)}: Finished downloading.");
        return content.Length;
    }
}
VA1267
  • 311
  • 1
  • 8

1 Answers1

3

Actually, async/await is not about threads (almost), but just about control flow control. So, in your code execution goes in a Main thread (which is not from a thread pool, by the way) until reaches await client.GetByteArrayAsync. Here the real low-level downloading is internally offloaded to the OS level and the program just waits the downloading result. And still no additional thread are spawned. But, when downloading finished, the .NET runtime want to continue execution after await. And here it can see no SynchronizationContext (as a console application does not has it) and then runtime executes the code after await in any thread available in thread pool. So, the rest of code after downloading will be executed in the thread from the pool.

If you will add a SynchronizationContext (or just move the code in WinForms app where the context exists out-of-the-box) you will see that all code will be executed in the main thread on no threads will be spawned/taken in from the thread pool as the runtime will see SynchronizationContext and will schedule after-await code on the original thread.

So, the answers

a) Main starts on the Main thread, not on the thread pool's thread. await itself does not actually spawn any threads. On the await, if the current thread was from thread pool, this thread will be put back in thread pool and will be available for future work. There is an exception, when the await will continue immediately and synchronously (see below).

b) runtime decides on which thread execution will be continued after 'await' depending of the current SynchronizationContext, ConfigureAwait settings and the availability of the operation result on the moment of reaching await.

In particular

  • if SynchronizationContext present and ConfigureAwait is set to true (or omitted), then code always continue in the current thread.

  • if SynchronizationContext does not present or ConfigureAwait is set to false, code will continue in any available thread (main thread or thread pool)

  • if you write something like

    var task = DoSomeWorkAsync();
    //some synchronous work which takes a while
    await task;
    

    then you can have a situation, when task is already finished on the moment when the code reaches await. In this case runtime can continue execution after await synchronously in the same thread. But this case is implementation-specific, as I know.

  • additionally, this is a special class TaskCompletionSource<TResult> (docs here) which provides explicit control over the task state and, in particular, may switch execution on any thread selected by the code owning TaskCompletionSource instance (see sample in @TheodorZoulias comment or here).

Serg
  • 3,454
  • 2
  • 13
  • 17
  • 1
    *"runtime executes the code after await in any thread available in thread pool."* -- This is not exactly true. The thread on which the continuation after the `await` will run depends on how the asynchronous method is implemented. It's entirely possible to run an a non-`ThreadPool` thread, as it's demonstrated [here](https://dotnetfiddle.net/XP44GF). – Theodor Zoulias Jan 16 '22 at 14:14
  • 1
    Thank you for interesting trick! I knew that `TaskCompletionSource` may be tricky, but this trick surpassed my imagination. And thank you for the edits, it's very odd how could I left so many typos in the text. I also added short note about `TaskCompletionSource` in the answer. – Serg Jan 16 '22 at 15:13
  • By the way, is there something similar to `TaskCompletionSource` (except custom `SynchronizationContext`, `TaskContinuationOptions`) which can override the way how the task continuation will be scheduled and executed? – Serg Jan 16 '22 at 15:19
  • 1
    Serg I don't know of anything else that is public and built-in the standard libraries. But I know the [`SwitchTo`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.visualstudio.threading.awaitextensions.switchto) method from the [Microsoft.VisualStudio.Threading](https://www.nuget.org/packages/Microsoft.VisualStudio.Threading/) package, which can switch to any desirable `TaskScheduler` context (built-in or custom). – Theodor Zoulias Jan 16 '22 at 15:38