ASP.NET中多个可等待任务导致Task.WaitAll挂起

27

以下是我遇到问题的代码的简化版本。当我在控制台应用程序中运行此代码时,它按预期工作。所有查询并行运行,Task.WaitAll()在它们全部完成后返回。

然而,当此代码在 Web 应用程序中运行时,请求会一直挂起。当我附加调试器并断开所有连接时,它显示执行正在等待 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个回答

53
与其使用 Task.WaitAll,您需要使用 await Task.WhenAll
在 ASP.NET 中,您有一个实际的同步上下文。这意味着,在所有 await 调用之后,您将被传回该上下文以执行继续(有效地序列化这些继续)。在控制台应用中没有同步上下文,因此所有的继续都只是被发送到线程池中。通过在请求的上下文中使用 Task.WaitAll,您正在阻塞它,从而防止它用于处理来自所有其他任务的继续。
还要注意,在 ASP 应用程序中异步/等待的主要好处之一是不要阻塞您正在使用来处理请求的线程池线程。如果您使用 Task.WaitAll,您将破坏这个目的。
进行此更改的一个副作用是,通过从阻塞操作移动到等待操作,异常将以不同的方式传播。而不会抛出 AggregateException,而是抛出其中的一个基础异常。

1
+1。我会使用"请求上下文"这个术语,而不是"主线程"。 - Stephen Cleary
4
重要提示:await Task.WhenAll 不会引发 AggregateException,只有其中一个内部异常将被传播。如果您想检查整个 AggregateException,则需要存储从 task.WhenAll 返回的任务的引用,并显式地检查其 Exception 属性。 - Dan Bryant
1
如果是这样的话,那么你最好完全删除所有的async/await代码,并使用阻塞方法串行执行所有操作。 - Servy
1
@sectrean 是的,这基本上是使用async/await的效果,所有获取或使用异步操作结果的内容都是任务,除非你从上到下进行更深入的重构。 - Servy
1
@Servy 在 ASP.NET 中,await 后会返回到相同的上下文,但可能在不同的线程上执行。因此,我认为你不能真正称其为“该请求的主线程”,因为该线程在请求的生命周期内可能会发生变化。 - svick
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接