Мне нужно запустить три асинхронных операции ввода-вывода параллельно, особенно это вызовы базы данных. Итак, я пишу следующий код:
// I need to know whether these tasks started running here
var task1 = _repo.GetThingOneAsync();
var task2 = _repo.GetThingTwoAsync();
var task3 = _repo.GetThingThreeAsync();
// await the results
var task1Result = await task1;
var task2Result = await task2;
var task3Result = await task3;
Методы GetThingOneAsync(), GetThingTwoAsync(), GetThingThreeAsync()
очень похожи друг на друга, за исключением того, что они имеют разные типы возврата (Task<string>, Task<int>, Task<IEnumerable<int>>
). Пример одного из вызовов базы данных следующий:
public async Task<IEnumerable<int>> GetThingOneAsync(string code)
{
return await db.DrType.Where(t => t.Code == code).Select(t => t.IdType).ToListAsync();
}
В режиме отладки я вижу, что var task1 = _repo.GetThingOneAsync();
начал запускать GetThingOneAsync()
асинхронный метод (то же самое с двумя другими задачами).
Мои коллеги говорят, что _repo.GetThingOneAsync()
не запускает асинхронную операцию. Говорят, что операция началась, когда мы достигли await
(это утверждение кажется мне неверным).
Итак, они предлагают исправить мой код следующим образом:
var task1 = _repo.GetThingOneAsync();
var task2 = _repo.GetThingTwoAsync();
var task3 = _repo.GetThingThreeAsync();
await Task.WhenAll(task1, task2, task3);
// then get the result of each task through `Result` property.
По-моему, то же самое, что я написал в самом начале вопроса, за исключением того, что Task.WhenAll
ожидает завершения более поздних задач, если более ранняя задача дает сбой (это комментарий Серви от этот вопрос)
Я знаю, что мой вопрос дублируется, но я хочу знать, правильно ли я делаю что-то или неправильно.
Просто убедитесь, что ваш уровень доступа к данным поддерживает многопоточность. Например, инфраструктура Entity не.
Я согласен с @MickyD, задачи были созданы при первом звонке. Эти два вызова похожи по действию.
Хотя есть несколько нюансов. Когда вы вызываете метод GetThingOneAsync, он выполняется до тех пор, пока не достигнет оператора ожидания; то есть, когда он возвращает Task. Если метод Async никогда не выполняет await, он завершает работу и возвращает уже завершенную задачу. Так что, если бы это были подпрограммы с интенсивным вычислением (не похоже на это), вы бы не достигли какого-либо параллелизма. Вам нужно будет использовать Task.Run для одновременного выполнения. Другой момент заключается в том, что если вы используете await из потока пользовательского интерфейса, то все выполнение будет происходить в потоке пользовательского интерфейса, просто запланированном в разное время. Это нормально, если задача выполняет ввод-вывод, потому что она будет блокироваться для чтения/записи. Однако он может начать накапливаться, поэтому, если вы собираетесь делать что-то существенное, вам следует поместить его в пул потоков (например, с помощью Task.Run).
Что касается комментария от ваших коллег, как я уже сказал, задача 1,2,3 запускается до ожидания. Но когда вы нажмете await, метод, который вы сейчас выполняете, приостановится и вернет Task. Таким образом, в какой-то мере правильно, что именно ожидание создает задачу - просто задача, о которой вы думаете в своем вопросе (задача 1,2,3), создается, когда GetThingXxxAsync попадает в ожидание, а не та, которая создается, когда ваш основная процедура ожидает задачи1,2,3.
Ну, я имел в виду, что задача блокируется до завершения ввода-вывода; поток не заблокирован.
My colleagues say that _repo.GetThingOneAsync() does not start the async operation. They say that the operation started when we reach await (that statement seems to be wrong for me).
Они ошибаются. Операция начинается при вызове метода.
Это легко доказать, запустив операцию, имеющую некоторый наблюдаемый побочный эффект (например, запись в базу данных). Вызовите метод, а затем заблокируйте приложение, например, с помощью Console.ReadKey()
. Затем вы увидите, что операция завершена (в базе данных) без await
.
Остальная часть вопроса касается стилистических предпочтений. Между этими вариантами есть небольшая семантическая разница, но обычно это не важно.
var task1 = _repo.GetThingOneAsync();
var task2 = _repo.GetThingTwoAsync();
var task3 = _repo.GetThingThreeAsync();
// await the results
var task1Result = await task1;
var task2Result = await task2;
var task3Result = await task3;
Приведенный выше код будет await
(асинхронно ждать) завершения каждой задачи по одной за раз. Если все три выполняются успешно, то этот код эквивалентен подходу Task.WhenAll
. Разница в том, что если у task1
или task2
есть исключение, то task3
никогда не ожидается.
Это мой личный фаворит:
var task1 = _repo.GetThingOneAsync();
var task2 = _repo.GetThingTwoAsync();
var task3 = _repo.GetThingThreeAsync();
await Task.WhenAll(task1, task2, task3);
var task1Result = await task1;
var task2Result = await task2;
var task3Result = await task3;
Мне нравится Task.WhenAll
, потому что это явно. Читая код, становится ясно, что он выполняет асинхронный параллелизм, потому что прямо здесь есть Task.WhenAll
.
Мне нравится использовать await
вместо Result
, потому что он более устойчив к изменениям кода. В частности, если кто-то, кому не нравится Task.WhenAll
, приходит и удаляет его, вы все равно в конечном итоге await
выполняете эти задачи вместо использования Result
, что может привести к взаимоблокировкам и переносу исключений в AggregateException
. Единственная причина, по которой Result
работает после Task.WhenAll
, заключается в том, что эти задачи уже выполнены и их исключения уже наблюдались.
Но это во многом мнение.
does not start the async operation
. Они говорят, что операция началась, когда мы достигли ожидания" - если под этим подразумевается "ожидание создает задачу" то ваши коллеги не правы.Task
уже запущен до того, как управление покинет метод. Это началось независимо от того, сделали вы этоawait
или нет. stackoverflow.com/q/43089372/585968