8

Прочитал несколько источников про async/await, где писалось, что якобы никаких дополнительных потоков эти конструкции не создают.

Решил написать тестовый код:

 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");
            }
        }

Так вот, Visual Studio показывает, что было создано 2 потока.

  1. Главный поток, который ушел обрабатывать один из асинхронных методов
  2. Другой был создан для обработки второй асихронной задачи

А иногда, если верить дебагеру, их 3.

Как так?

Или все таки потоки не создаются при использовании только IO операций, а при каких-то вычислительных потоках CLR определяет необходимость создания потока?

Или под "не создает потоков" понималось, что берутся готовые потоки из пула, но множество потоков в программе в единицу времени имеет место быть?

Или имеется в виду, что старается использовать, как можно меньше потоков? Так если крутится 2 асинхронных операции и они не пресекаются по времени выполнения, то используется 1 поток, а если они параллельно крутятся, то CLR выгодно 2 потока крутить?

andreycha
  • 25,167
  • 4
  • 46
  • 82
iluxa1810
  • 24,899
  • 4
    Главный поток обрабатывает только первые синхронные части обоих асинхронных методов, после чего в обработке этих методов никак не участвует. Для выполнения каждой синхронной части асинхронного метода нужен поток, который будет её выполнять. При отсутствии контекста синхронизации и нестандартного планировщика TaskAwaiter.OnCompleted по завершении Task просто помещает в пул потоков задачу на выполнение следующей синхронной части приостановленного метода. Далее пул потоков сам решает как (создавать или нет новый поток и так далее) ему выполнять помещённые в него задачи. – user181245 Feb 02 '18 at 00:32
  • @PetSerAl, оформите как ответ. – iluxa1810 Feb 02 '18 at 05:19
  • async/await не имеет абсолютно никакого смысла в приложении без обработки сообщений – MSDN.WhiteKnight Feb 02 '18 at 07:03
  • @MSDN.WhiteKnight ты имеешь ввиду приложения с GUI – iluxa1810 Feb 02 '18 at 07:29
  • Ага. Весь их смысл, чтобы ждать без замораживания UI. У вас можно все это выпилить и оставить Thread.Sleep(1000) - эффект будет тот же. Что касается отдельных потоков, только Task с параметром LongRunning создает их. Обычные крутятся на потоках из ThreadPool. – MSDN.WhiteKnight Feb 02 '18 at 07:39
  • 1
    @MSDN.WhiteKnight Абсолютно неверно. Асинхронные операции не занимают процессор (и не фризят потоки), потому, например, всякие веб приложения могут обрабатывать больше запросов. Так что async/await полезно не только для гуи-приложений. – tym32167 Feb 02 '18 at 09:47
  • @PetSerAl имеет смысл разместить комментарий как ответ. – andreycha Feb 02 '18 at 15:50
  • @tym32167 ожидающие потоки и так не занимают процессор. Если что-то и экономится за счет снижения числа потоков, это память, а не процессорное время. – MSDN.WhiteKnight Feb 03 '18 at 12:09
  • @MSDN.WhiteKnight Thread.Sleep фризит поток, так? То есть полезная работа в этом потоке уже не выполняется, так как поток ждет. Или выполняется в новом потоке. А мы знаем, что чем больше потоков - тем больше фризов, верно (из за переключения контекстов процессором)? Ну, и, если я верно помню, что в asp.net есть ограничение на количество потоков в пуле. То есть с Thread.Sleep полезная работа либо не выполняется, либо требует больше ресурсов для выполнения. В случае асинхронной операции потоки не заняты и доп работа не ждет и не создает новый поток. Отсюда и экономия. – tym32167 Feb 03 '18 at 15:52
  • Я может капитан но... async/await это асинхронность, а асинхронность не равно многопоточность. Можно написать код так что даже асинхронное чтение файла не будет создавать потоков. Правда для этого придется вручную вызывать winapi. – Vasek Feb 03 '18 at 20:39

2 Answers2

10

Главный поток у вас висит на операции t.Wait(); и ничего не выполняет.

Вы не установили контекст синхронизации - а потому все продолжения await выполняются в пуле потоков. Отсюда и второй поток - для того чтобы выполнять вывод в консоль. А если обе задачи просыпаются одновременно - то нужен и третий поток.

Тем не менее, как вы можете заметить, Task.Delay(1000) сам по себе ни в каком потоке не выполняется - потоки нужны только для вывода в консоль. Если вы запустите десять тысяч подобных задач - им для выполнения будет достаточно относительно небольшого числа потоков. В этом и выгода.

Кстати, способ избавиться от дополнительных потоков - есть, но для этого надо избавиться от вызова .Wait() и поставить какой-нибудь контекст синхронизации.

Например, можно взять QueueSynchronizationContext из моего ответа на вопрос "Зависает оператор await в оконном приложении / программа висит при вызове Task.Result или Wait". Вот такой код будет выполняться строго в одном потоке:

 public static void Main()
 {
     var syncCtx = new QueueSynchronizationContext(); 
     // вызывает внутри SynchronizationContext.SetSynchronizationContext(syncCtx);

     var t = Test(); // Важно: вызывать строго после SetSynchronizationContext

     syncCtx.WaitFor(t);
 }
Pavel Mayorov
  • 58,537
4

Асинхронные методы по своей сути представляют последовательность синхронных блоков, которые могут прерываться асинхронным ожиданием. Для выполнения каждого синхронного блока нужен поток, который будет его выполнять. Но для асинхронного ожидания поток не нужен, в этом собственно и заключается смысл асинхронных методов. async фактически отвечает только за то, чтобы переписать метод таким образом, чтобы он мог быть приостановлен и возобновлён в точках вызова оператора await.

За выполнение первого синхронного блока асинхронного метода отвечает вызвавший его метод. Когда работу асинхронного метода необходимо приостановить, он создаёт делегат, представляющий выполнение следующей синхронной части, и передаёт его в реализацию метода INotifyCompletion.OnCompleted, а сам завершает работу и возвращает управление вызвавшему коду. Конкретная реализация интерфейса INotifyCompletion получается по средством вызова GetAwaiter на аргументе оператора await. В дальнейшем именно реализация метода OnCompleted определяет когда и в каком потоке будет выполнен следующий синхронный блок асинхронного метода.

В Вашем примере главный поток отвечает только за выполнение первых синхронных блоков каждого из асинхронных методов. Далее он блокируется на вызове t.Wait() и в дальнейшей обработке асинхронных методов не участвует. Вы используете оператор await на объектах типа Task, поэтому реализацией интерфейса INotifyCompletion будет TaskAwaiter. Также у Вас отсутствует нестандартный контекст синхронизации и планировщик, поэтому TaskAwaiter.OnCompleted по завершении соответствующего Task объекта просто добавит задачу выполнения следующего синхронного блока в пул потоков. Далее пул потоков сам принимает решение о том как ему выполнять эту задачу: создавать для неё дополнительный поток или нет.

user181245
  • 1,454