У меня есть асинхронный метод для обработки агрегированных запросов. По сути, он разбивает запросы на отдельные, обрабатываемые другим методом с тем же типом возвращаемого значения, и объединяет ответы в один ответ. Если совокупный запрос на самом деле состоит только из одного запроса, я могу делегировать его непосредственно второму методу.
Когда я это делаю, поскольку мой метод async
, я не могу вернуть Task<string>
из другого метода напрямую, мне нужно это await
, хотя мой метод ничего не делает с результатом. С другой стороны, в ветке, которая агрегирует ответы, мне действительно нужно получить результаты, чтобы сгенерировать агрегированный ответ, поэтому мне нужно await
там.
Необходимость использования return await
кажется мне неестественной и, похоже, лишает вызывающую сторону возможности выполнить какую-либо собственную оптимизацию.
Мои вопросы:
Возможно ли/рекомендуется избегать return await
в ветке, которая делегирует Task
с тем же типом возвращаемого значения? Например, не создавая метод async
и ожидая Task
во второй ветке, используя task.GetAwaiter().GetResult()
?
Или мне не стоит об этом беспокоиться, потому что никакой заметной разницы в конечном результате для вызывающего абонента никогда не будет?
В конце концов, результата все-таки придется ждать.
Этот пример был упрощен для использования простых строковых операций для демонстрации идеи; на самом деле мой метод значительно сложнее.
public async Task<string> ProcessCombinedRequest(List<string> requestList)
{
if (requestList.Count == 1)
{
return await ProcessSingleRequest(requestList[0]);
}
// Start all the sub-tasks concurrently
var taskList = new List<Task<string>>();
foreach (string request in requestList)
{
taskList.Add(ProcessSingleRequest(request));
}
// Wait for each of them to finish and get their results
var aggregatedResponse = new StringBuilder();
foreach (var task in taskList)
{
string singleResponse = await task;
aggregatedResponse.AppendLine(singleResponse);
}
return aggregatedResponse.ToString();
}
private async Task<string> ProcessSingleRequest(string request)
{
// Long running operation
await Task.Delay(4000);
return request + " was processed.";
}
Обновлено: изменил Thread.Sleep
на await Task.Delay
, как предложил @TheodorZoulias, чтобы улучшить пример.
Это то, что я читал в другом месте, но у меня не было других идей.
Вы рассматривали возможность использовать var results = await Task.WhenAll( tasks );
, а затем пойти дальше? Я бы действительно не стал создавать специальную ветвь для n = 1, за исключением случаев, когда появление n = 1 перевешивает любое другое хотя бы на порядок. И даже в этом случае я бы сначала тщательно протестировал.
Случай n=1 является наиболее распространенным.
В качестве примечания: метод ProcessSingleRequest
должен выдавать предупреждение компиляции о методе async
, в котором отсутствует оператор await
. Представление длительной операции с помощью await Task.Delay(4000)
может быть лучше, чем Thread.Sleep(4000)
. Также опускать Task.WhenAll
неправильно. Вы рискуете потерять контроль над своими задачами и позволить им превратиться в «выстрелил и забыл».
«Необходимость использования return await
кажется мне неестественной и, похоже, лишает вызывающую сторону возможности выполнить какую-либо собственную оптимизацию». - Не могли бы вы объяснить, почему вы пришли к такому выводу?
Использование return await
означает, что мой метод активно ожидает результата перед возвратом. Если вместо этого я верну горячую задачу, вызывающая сторона сможет запланировать ее вместе с другой работой и дождаться результата в самый последний момент.
Да, но есть нюансы и нюансы. Вы можете использовать метод, отличный от async
:
public Task<string> ProcessCombinedRequest(List<string> requestList)
{
// TODO: consider ternary conditional or switch expression, lambda, etc
// TODO: consider the empty list case?
if (requestList.Count == 1)
{
return ProcessSingleRequest(requestList[0]);
}
return ProcessCombinedRequestMulti(requestList);
}
private async Task<string> ProcessCombinedRequestMulti(List<string> requestList)
{
// Start all the sub-tasks concurrently
var taskList = new List<Task<string>>();
// .. etc as before
}
Это устраняет накладные расходы конечного автомата, но, что важно, это также меняет поведение в случае исключений - например, если requestList
оказывается null
или происходит какая-то другая случайная ошибка - вместо возврата ошибочного Task<string>
ваш метод может throw
— и в любом случае наблюдаемая трассировка стека может незначительно отличаться.
Если вы уверены, что такого риска нет (возможно, с некоторыми проверками Debug.Assert
и т. д.), а версия без async
, вероятно, будет очень распространена, и производительность имеет значение: вы, безусловно, можете рассмотреть это. Однако, если вы не уверены в эффекте: возможно, просто не беспокойтесь об этом...
Чтобы привести конкретный пример: https://github.com/dotnet/runtime/blob/ea9d53e694671ca8c70d61eb1df8779959045af6/src/libraries/Microsoft.Extensions.Caching.Abstractions/src/Hybrid/HybridCache.cs#L74-L82
Быстро, лаконично и хорошо ответили! Я исключил крайний случай пустого списка, чтобы упростить задачу, но на самом деле он включен в foreach
, который здесь синхронно возвращает пустую строку ;-).
@ChristopherHamkins в исходном коде: да, синхронно, но после раскрутки половины механизма async
(конечного автомата) и StringBuilder
;p Короткое замыкание перед попаданием в блок async
сохраняет довольно много движущихся частей. Если вы автор библиотеки и этот метод вызывается лотами (возможно, внутри циклов), то: стоит оптимизировать; если вы являетесь автором приложения, и это (например) один раз за запрос или один раз за изменение экрана пользовательского интерфейса или нажатие кнопки: вероятно, не стоит об этом беспокоиться
«и ожидание задач во второй ветке с помощью Task.GetAwaiter().GetResult()» — категорически не рекомендуется.