Страницы

Поиск по вопросам

воскресенье, 29 декабря 2019 г.

async/await и создание потоков

#c_sharp #net #async_await


Прочитал несколько источников про 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 потока.


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


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

Как так? 

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

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

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


Ответы

Ответ 1



Главный поток у вас висит на операции 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); }

Ответ 2



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

Комментариев нет:

Отправить комментарий