I read several sources about async / await, where it was written that these constructions supposedly do not create any additional threads.

I decided to write a test code:

public static void Main() { var t = Test(); t.Wait(); } static async Task Test() { var t = Test2(); for (; ; ) { await Task.Delay(1000); Console.WriteLine("0"); } } static async Task Test2() { for (; ; ) { await Task.Delay(1000); Console.WriteLine("1"); } } 

So, Visual Studio shows that 2 threads were created.

  1. The main thread that went to handle one of the asynchronous methods
  2. Another was created to handle the second asychron task.

And sometimes, according to the debager, there are 3 of them.

How so?

Or all the same, threads are not created using only IO operations, and with some computational threads, does the CLR determine the need to create a stream?

Or was it understood by “does not create threads” that ready-made threads are taken from the pool, but do you have a lot of threads in a program at a time?

Or mean that tries to use as few streams as possible? So if 2 asynchronous operations are spinning and they are not suppressed by the execution time, then 1 thread is used, and if they are spinning in parallel, then is CLR advantageous to turn 2 threads?

  • four
    The main thread processes only the first synchronous parts of both asynchronous methods, after which it does not participate in the processing of these methods. To execute each synchronous part of the asynchronous method, you need a thread that will execute it. In the absence of a synchronization context and a nonstandard scheduler, TaskAwaiter.OnCompleted upon completion of Task simply places the task in the thread pool for the execution of the next synchronous part of the suspended method. Next, the thread pool itself decides how (whether or not to create a new thread, and so on) for it to perform the tasks placed in it. - PetSerAl
  • @PetSerAl, issue as an answer. - iluxa1810
  • async / await makes absolutely no sense in an application without processing messages - MSDN.WhiteKnight
  • @ MSDN.WhiteKnight you mean applications with GUI - iluxa1810
  • one
    @ MSDN.WhiteKnight Absolutely incorrect. Asynchronous operations do not occupy the processor (and do not freeze threads), because, for example, all web applications can handle more requests. So async / await is not only useful for gui applications. - tym32167

2 answers 2

Your main thread is hanging on operation t.Wait(); and does nothing.

You have not set the synchronization context - and therefore all await sequels are executed in the thread pool. Hence the second thread - in order to perform the output to the console. And if both tasks wake up at the same time, then a third thread is needed.

However, as you can see, Task.Delay(1000) itself is not running on any thread — threads are needed only for output to the console. If you run ten thousand of these tasks, a relatively small number of threads will be enough for them to perform. This is a benefit.

By the way, there is a way to get rid of additional threads, but for this you need to get rid of the .Wait() call and put some kind of synchronization context.

For example, you can take the QueueSynchronizationContext from my answer to the question "The await operator hangs in a window application / program hangs when you call Task.Result or Wait . " Here such code will be executed strictly in one thread:

  public static void Main() { var syncCtx = new QueueSynchronizationContext(); // вызывает внутри SynchronizationContext.SetSynchronizationContext(syncCtx); var t = Test(); // Важно: вызывать строго после SetSynchronizationContext syncCtx.WaitFor(t); } 
  • Why not async Main? - VladD
  • @VladD has not touched this feature yet, I can not write anything about it. - Pavel Mayorov

Asynchronous methods are essentially a sequence of synchronous blocks that can be interrupted by asynchronous waiting. To execute each synchronous block you need a thread that will execute it. But for asynchronous waiting the flow is not needed, this is the essence of asynchronous methods. async actually only responsible for rewriting the method so that it can be suspended and resumed at the call points of the await operator.

The method that caused it is responsible for executing the first synchronous block of the asynchronous method. When the work of the asynchronous method needs to be suspended, it creates a delegate representing the execution of the next synchronous part, and passes it to the implementation of the INotifyCompletion.OnCompleted method, and ends the work and returns control to the calling code. The concrete implementation of the INotifyCompletion interface INotifyCompletion obtained by calling GetAwaiter on the argument of the await operator. In the future, it is the implementation of the OnCompleted method OnCompleted determines when and in which thread the next synchronous block of the asynchronous method will be executed.

In your example, the main thread is only responsible for executing the first synchronous blocks of each of the asynchronous methods. Further, it blocks on the t.Wait() call and does not participate in further processing of asynchronous methods. You use the await operator on objects of type Task , so the implementation of the INotifyCompletion interface will be TaskAwaiter . Also, you have no nonstandard synchronization context and scheduler, so TaskAwaiter.OnCompleted after completing the corresponding Task object will simply add the task of executing the next synchronous block to the thread pool. Next, the thread pool itself decides how to perform this task: whether to create an additional thread for it or not.