Запуск асинхронной операции параллельно без Task.WhenAll

Мне нужно запустить три асинхронных операции ввода-вывода параллельно, особенно это вызовы базы данных. Итак, я пишу следующий код:

// 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ожидает завершения более поздних задач, если более ранняя задача дает сбой (это комментарий Серви от этот вопрос)

Я знаю, что мой вопрос дублируется, но я хочу знать, правильно ли я делаю что-то или неправильно.

"Мои коллеги говорят, что _repo.GetThingOneAsync() does not start the async operation. Они говорят, что операция началась, когда мы достигли ожидания" - если под этим подразумевается "ожидание создает задачу" то ваши коллеги не правы. Task уже запущен до того, как управление покинет метод. Это началось независимо от того, сделали вы это await или нет. stackoverflow.com/q/43089372/585968
MickyD 29.07.2019 14:30

Просто убедитесь, что ваш уровень доступа к данным поддерживает многопоточность. Например, инфраструктура Entity не.

Theodor Zoulias 29.07.2019 16:01
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
2
438
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Я согласен с @MickyD, задачи были созданы при первом звонке. Эти два вызова похожи по действию.

Хотя есть несколько нюансов. Когда вы вызываете метод GetThingOneAsync, он выполняется до тех пор, пока не достигнет оператора ожидания; то есть, когда он возвращает Task. Если метод Async никогда не выполняет await, он завершает работу и возвращает уже завершенную задачу. Так что, если бы это были подпрограммы с интенсивным вычислением (не похоже на это), вы бы не достигли какого-либо параллелизма. Вам нужно будет использовать Task.Run для одновременного выполнения. Другой момент заключается в том, что если вы используете await из потока пользовательского интерфейса, то все выполнение будет происходить в потоке пользовательского интерфейса, просто запланированном в разное время. Это нормально, если задача выполняет ввод-вывод, потому что она будет блокироваться для чтения/записи. Однако он может начать накапливаться, поэтому, если вы собираетесь делать что-то существенное, вам следует поместить его в пул потоков (например, с помощью Task.Run).

Что касается комментария от ваших коллег, как я уже сказал, задача 1,2,3 запускается до ожидания. Но когда вы нажмете await, метод, который вы сейчас выполняете, приостановится и вернет Task. Таким образом, в какой-то мере правильно, что именно ожидание создает задачу - просто задача, о которой вы думаете в своем вопросе (задача 1,2,3), создается, когда GetThingXxxAsync попадает в ожидание, а не та, которая создается, когда ваш основная процедура ожидает задачи1,2,3.

Это нормально, если Задача выполняет ввод-вывод, потому что она будет блокироваться для чтения/записи. Это утверждение неверно, если предположить, что код правильно ожидает любого ввода-вывода. Весь смысл async/await заключается в том, чтобы избежать блокировки. Может быть, вы имели в виду «освободить поток» вместо «заблокировать»?
juharr 29.07.2019 15:50

Ну, я имел в виду, что задача блокируется до завершения ввода-вывода; поток не заблокирован.

sjb-sjb 30.07.2019 01:59
Ответ принят как подходящий

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, заключается в том, что эти задачи уже выполнены и их исключения уже наблюдались.

Но это во многом мнение.

Другие вопросы по теме