Можно ли избежать `return await` в ветвях, которые делегируют задачу с тем же типом возвращаемого значения?

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

Когда я это делаю, поскольку мой метод async, я не могу вернуть Task<string> из другого метода напрямую, мне нужно это await, хотя мой метод ничего не делает с результатом. С другой стороны, в ветке, которая агрегирует ответы, мне действительно нужно получить результаты, чтобы сгенерировать агрегированный ответ, поэтому мне нужно await там.

Необходимость использования return await кажется мне неестественной и, похоже, лишает вызывающую сторону возможности выполнить какую-либо собственную оптимизацию.

Мои вопросы:

  1. Возможно ли/рекомендуется избегать return await в ветке, которая делегирует Task с тем же типом возвращаемого значения? Например, не создавая метод async и ожидая Task во второй ветке, используя task.GetAwaiter().GetResult()?

  2. Или мне не стоит об этом беспокоиться, потому что никакой заметной разницы в конечном результате для вызывающего абонента никогда не будет?

В конце концов, результата все-таки придется ждать.

Этот пример был упрощен для использования простых строковых операций для демонстрации идеи; на самом деле мой метод значительно сложнее.

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, чтобы улучшить пример.

«и ожидание задач во второй ветке с помощью Task.GetAwaiter().GetResult()» — категорически не рекомендуется.

Fildor 19.08.2024 17:24

Это то, что я читал в другом месте, но у меня не было других идей.

Christopher Hamkins 19.08.2024 17:26

Вы рассматривали возможность использовать var results = await Task.WhenAll( tasks );, а затем пойти дальше? Я бы действительно не стал создавать специальную ветвь для n = 1, за исключением случаев, когда появление n = 1 перевешивает любое другое хотя бы на порядок. И даже в этом случае я бы сначала тщательно протестировал.

Fildor 19.08.2024 17:36

Случай n=1 является наиболее распространенным.

Christopher Hamkins 19.08.2024 17:37
Исключение async/await имеет некоторые особенности, особенно когда дело касается исключений. Так что я бы не рекомендовал это беззаботно. Итак, если это не реальная проблема, я бы просто оставил async/await. Возможно, есть и другие вещи, которые можно попробовать, которые дадут больший эффект с меньшим риском.
Fildor 19.08.2024 17:46

В качестве примечания: метод ProcessSingleRequest должен выдавать предупреждение компиляции о методе async, в котором отсутствует оператор await. Представление длительной операции с помощью await Task.Delay(4000) может быть лучше, чем Thread.Sleep(4000). Также опускать Task.WhenAll неправильно. Вы рискуете потерять контроль над своими задачами и позволить им превратиться в «выстрелил и забыл».

Theodor Zoulias 19.08.2024 19:59

«Необходимость использования return await кажется мне неестественной и, похоже, лишает вызывающую сторону возможности выполнить какую-либо собственную оптимизацию». - Не могли бы вы объяснить, почему вы пришли к такому выводу?

Guru Stron 20.08.2024 09:36

Использование return await означает, что мой метод активно ожидает результата перед возвратом. Если вместо этого я верну горячую задачу, вызывающая сторона сможет запланировать ее вместе с другой работой и дождаться результата в самый последний момент.

Christopher Hamkins 20.08.2024 10:52
Стоит ли изучать 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
8
50
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Да, но есть нюансы и нюансы. Вы можете использовать метод, отличный от 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, который здесь синхронно возвращает пустую строку ;-).

Christopher Hamkins 19.08.2024 17:34

@ChristopherHamkins в исходном коде: да, синхронно, но после раскрутки половины механизма async (конечного автомата) и StringBuilder ;p Короткое замыкание перед попаданием в блок async сохраняет довольно много движущихся частей. Если вы автор библиотеки и этот метод вызывается лотами (возможно, внутри циклов), то: стоит оптимизировать; если вы являетесь автором приложения, и это (например) один раз за запрос или один раз за изменение экрана пользовательского интерфейса или нажатие кнопки: вероятно, не стоит об этом беспокоиться

Marc Gravell 20.08.2024 10:39

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