Будет ли это асинхронно, если я вызову несколько асинхронных методов без ожидания, но затем использую Task.WhenAll? - PullRequest
3 голосов
/ 30 октября 2019

У меня есть кусок кода, который я не совсем уверен, если это будет работать асинхронно. Ниже я составил несколько примеров сценариев, которые действительно отражают ситуацию. Обратите внимание, что методы GetAsync являются правильными asyn методами, имеющими ключевые слова async/await и тип возвращаемого значения с использованием Task связанного объекта.

public async Task<SomeResults> MyMethod() 
{
    var customers = _customerApi.GetAllAsync("some_url");
    var orders = _orderApi.GetAllAsync("some_url");
    var products = _productApi.GetAllAsync("some_url");
    await Task.WhenAll(customers, orders, products);
   // some more processing and returning the results
}

Вопрос 1. Будут ли три вышеуказанных вызова APIработать асинхронно, хотя перед ними нет await? Но у нас есть await до Task.WhenAll?

Вопрос 2. Будет ли приведенный выше код выполняться асинхронно, если ключевое слово await будет удалено до Task.WhenAll?

IЯ пытался найти его в Google, но не смог найти правильный ответ на эту конкретную ситуацию. Я начал читать Параллельное программирование в Microsoft .NET , но мне еще предстоит пройти длинный путь, поэтому я не мог просто подождать.

Ответы [ 3 ]

3 голосов
/ 30 октября 2019

Вопрос 1. Будут ли три вышеуказанных вызова API выполняться асинхронно, даже если перед ними нет await? Но у нас есть await до Task.WhenAll?

  1. Если методы на самом деле делают что-то асинхронно, тогда да.

Вопрос2: Будет ли приведенный выше код выполняться асинхронно, если ключевое слово await будет удалено до Task.WhenAll?

Если методы на самом деле делают что-то асинхронно, тогда да. Тем не менее, было бы бессмысленно использовать Task.WhenAll без await.

Почему я говорю "если": ключевое слово async волшебным образом не создает методВ асинхронном режиме оператор await также не . Методы все еще должны делать что-то асинхронно. Они делают это, возвращая неполные Task.

Все async методы запускаются синхронно, как и любой другой метод. Магия происходит в await. Если await задано неполное Task, то метод возвращает свое собственное неполное Task, а остальная часть метода подписана как продолжение этого Task. Это происходит на всем пути вверх по стеку вызовов, пока вы используете await на всем пути вверх к стеку вызовов.

Как только Task завершается, то выполняется продолжение (остальные методыпосле await).

Но на вершине стека вызовов должно быть что-то, что на самом деле является асинхронным. Если у вас есть метод async, который вызывает метод async, который вызывает синхронный метод, то на самом деле ничего не будет работать асинхронно, даже если вы используете await.

Например, он будет работать полностью синхронно(т. е. поток будет блокироваться), потому что неполное Task никогда нигде не возвращается:

async Task Method1() {
    await Method2();
}

async Task Method2() {
    await Method3();
}

Task Method3() {
    Thread.Sleep(2000);
    return Task.CompletedTask;
}

Однако это будет выполняться асинхронно (т. е. во время задержки поток освобождается длявыполнить другую работу):

async Task Method1() {
    await Method2();
}

async Task Method2() {
    await Method3();
}

async Task Method3() {
    await Task.Delay(2000);
}

Ключ находится в , что Task.Delay возвращает . Если вы посмотрите на этот исходный код, вы увидите, что он возвращает DelayPromise (который наследуется от Task) сразу (до истечения времени). Поскольку ожидается, это вызывает Method3 для возврата неполного Task. Поскольку Method2 ожидает этого, он возвращает неполный Task и т. Д. До конца стека вызовов.

2 голосов
/ 30 октября 2019

ДА, на оба вопроса много предостережений.

await / async - это просто синтаксический сахар, который позволяет вам писать асинхронный код синхронно. Это не волшебным образом раскручивает потоки, чтобы заставить вещи работать параллельно. Он просто позволяет освободить текущий исполняемый поток для выполнения других частей работы. Думайте о ключевом слове await как о паре ножниц, которые обрезают текущую часть работы на две части, что означает, что текущий поток может пойти и выполнить другую часть в ожидании результата.

Для выполнения этих частей работы, должен быть какой-то TaskScheduler . WinForms и WPF предоставляют TaskSchedulers, которые позволяют одному потоку обрабатывать чаны один за другим, но вы также можете использовать планировщик по умолчанию (через Task.Run()), который будет использовать пул потоков, что означает, что множество потоков будет одновременно запускать много чанков. ,

Если вы используете один поток, пример кода будет работать следующим образом:

_customerApi.GetAllAsync() будет работать до тех пор, пока не завершится или не достигнет await. В этот момент он вернется к Задаче к вашей вызывающей функции, которая будет вставлена ​​в customers.

_orderApi.GetAllAsync() будет работать точно так же. Задание будет назначено для заказов, которые могут быть или не быть завершенными.

то же самое _productApi.GetAllAsync()

затем вы нажмете await Task.WhenAll(customers, orders, products); это означает, что он может идти и делать другие вещи, поэтомуTaskScheduler может дать ему еще несколько кусков работы, например продолжить выполнение следующего бита _customerApi.GetAllAsync().

В конце концов все куски работы будут выполнены, и ваши три задачи внутри customers, orders и products будут выполнены. На этом этапе планировщик знает, что он может запустить бит после WhenAll ()

. Таким образом, вы можете видеть, что в этом случае отдельный поток выполнил весь код, но не обязательно синхронно.

1 голос
/ 31 октября 2019

То, выполняется ли ваш код асинхронно, зависит от вашего определения асинхронности. Если вы действительно внимательно посмотрите, что происходит, будет только один поток, который будет делать вещи. Однако этот поток не будет ждать без дела, пока у него есть чем заняться.

То, что действительно помогло мне понять async-await, - это аналогия с приготовлением завтрака в в этом интервью сЭрик Липперт . Найдите где-то посередине асинхронное ожидание.

Предположим, что повар готовит завтрак. Он начинает кипятить чай. Вместо того чтобы ждать, пока вода приготовится, он вставляет хлеб в тостер. Не дожидаясь ожидания, он начинает кипятить яйца. Как только чай закипает, он заваривает чай и ждет тост или яйца.

Асинхронное ожидание аналогично. Всякий раз, когда вашему потоку придется ждать завершения другого процесса, например, записи файла, запроса к базе данных для возврата данных, загрузки данных из Интернета, поток не будет бездействовать в ожидании завершения другого процесса, но будетподнимитесь по стеку вызовов, чтобы увидеть, не ожидает ли кто-либо из вызывающих абонентов, и начинает выполнять операторы, пока не увидит ожидание. Снова поднимитесь вверх по стеку вызовов и выполняйте до момента ожидания. и т.д.

Поскольку GetAllAsync объявлен асинхронным, вы можете быть уверены, что в нем есть ожидание. Фактически, ваш компилятор предупредит вас, если вы объявите функцию асинхронной без ожидания в ней.

Ваш поток перейдет в _customerApi.GetAllAsync("some_url"); и выполнит операторы, пока не увидит ожидание. Если задача, которую ожидает ваш поток, не завершена, поток поднимается вверх по стеку вызовов (ваша процедура) и начинает выполнять следующий оператор: _orderApi.GetAllAsync("some_url"). Он выполняет операторы, пока не увидит ожидание. Ваша функция снова получает управление и вызывает следующий метод.

Это продолжается до тех пор, пока ваша процедура не начнет ожидать. В этом случае ожидаемый метод Task.WhenAll (не путать с ненастоящим Task.WaitAll).

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

Это похоже на единственное блюдо: пока он вставляет хлеб в тостер, он не может обработать кипящую воду для чая: только после того, как хлеб будет вставлен, он начнетне дожидаясь, пока он поджарится, он может продолжать заваривать чай.

await Task.WhenAll ничем не отличается от других ожиданий, за исключением того, что задача завершена, когда все задачи завершены. Таким образом, пока ни одна из задач не готова, ваш поток не будет выполнять операторы после WhenAll. Однако: ваш поток не будет ждать бездействия, он поднимется в стек вызовов и начнет выполнять операторы.

Так что, хотя кажется, что два куска кода выполняются одновременно, это не так. Если вы действительно хотите, чтобы два куска кода выполнялись одновременно, вам придется нанять нового повара, используя `Task.Run (() => SliceTomatoes);

Наем нового повара (начало нового потока) - этоимеет смысл только в том случае, если другая задача не является асинхронной, и у вашего потока есть другие важные действия, такие как поддержание адаптивности пользовательского интерфейса. Обычно твой повар сам бы нарезал помидоры. Пусть ваш абонент решит, нанять ли он нового повара (= вас), чтобы приготовить завтрак и нарезать помидоры.

Я немного упростил это, сказав, что здесь задействована только одна тема (повар). Фактически, это может быть любой поток, который продолжает выполнять операторы после вашего ожидания. Вы можете увидеть, что в отладчике, изучив идентификатор потока, довольно часто это будет другой поток, который будет продолжаться. Однако этот поток имеет тот же контекст, что и ваш исходный поток, поэтому для вас это будет так же, как если бы это был тот же поток: нет необходимости в мьютексе, нет необходимости в IsInvokeRequired для потоков пользовательского интерфейса. Более подробную информацию об этом можно найти в статьях Стивена Клири

...