Task.WaitAll висит с несколькими ожидаемыми задачами в ASP.NET

Ниже приведен упрощенный вариант кода, с которым я столкнулся. Когда я запускаю его в консольном приложении, он работает так, как ожидалось. Все запросы выполняются параллельно, а Task.WaitAll() возвращается, когда все они завершены.

Однако, когда этот код работает в веб-приложении, запрос просто зависает. Когда я присоединяю отладчик и разбиваю все, он показывает, что выполнение выполняется на Task.WaitAll(). И первая задача завершена, но другие никогда не заканчиваются.

Я не могу понять, почему он зависает при работе в ASP.NET, но отлично работает в консольном приложении.

public Foo[] DoWork(int[] values)
{
    int count = values.Length;
    Task[] tasks = new Task[count];

    for (int i = 0; i < count; i++)
    {
        tasks[i] = GetFooAsync(values[i]);
    }

    try
    {
        Task.WaitAll(tasks);
    }
    catch (AggregateException)
    {
        // Handle exceptions
    }

    return ...
}

public async Task<Foo> GetFooAsync(int value)
{
    Foo foo = null;

    Func<Foo, Task> executeCommand = async (command) =>
    {
        foo = new Foo();

        using (SqlDataReader reader = await command.ExecuteReaderAsync())
        {
            ReadFoo(reader, foo);
        }
    };

    await QueryAsync(executeCommand, value);

    return foo;
}

public async Task QueryAsync(Func<SqlCommand, Task> executeCommand, int value)
{
    using (SqlConnection connection = new SqlConnection(...))
    {
        connection.Open();

        using (SqlCommand command = connection.CreateCommand())
        {
            // Set up query...

            await executeCommand(command);

            // Log results...

            return;
        }
    }           
}

Ответ 1

Вместо Task.WaitAll вам нужно использовать await Task.WhenAll.

В ASP.NET у вас есть реальный контекст синхронизации. Это означает, что после всех вызовов await вы будете перенаправлены обратно в этот контекст для выполнения продолжения (эффективно сериализуя эти продолжения). В консольном приложении нет контекста синхронизации, поэтому все продолжения просто отправляются в пул потоков. Используя Task.WaitAll в контексте запроса, вы блокируете его, что предотвращает его использование для обработки продолжений из всех других задач.

Также обратите внимание, что одним из основных преимуществ async/await в приложении ASP является не блокирование потока потока потоков, который вы используете для обработки запроса. Если вы используете Task.WaitAll, вы побеждаете эту цель.

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