一尘不染

为什么此异步操作挂起?

c#

我有一个多层.Net 4.5应用程序,它使用C#的new asyncawait挂起的关键字来调用方法,但我不知道为什么。

在底部,我有一个异步方法来扩展我们的数据库实用程序OurDBConn(基本上是基础DBConnectionDBCommand对象的包装器):

public static async Task<T> ExecuteAsync<T>(this OurDBConn dataSource, Func<OurDBConn, T> function)
{
    string connectionString = dataSource.ConnectionString;

    // Start the SQL and pass back to the caller until finished
    T result = await Task.Run(
        () =>
        {
            // Copy the SQL connection so that we don't get two commands running at the same time on the same open connection
            using (var ds = new OurDBConn(connectionString))
            {
                return function(ds);
            }
        });

    return result;
}

然后,我有一个中级异步方法,调用此方法可获得一些运行缓慢的总数:

public static async Task<ResultClass> GetTotalAsync( ... )
{
    var result = await this.DBConnection.ExecuteAsync<ResultClass>(
        ds => ds.Execute("select slow running data into result"));

    return result;
}

最后,我有一个同步运行的UI方法(MVC操作):

Task<ResultClass> asyncTask = midLevelClass.GetTotalAsync(...);

// do other stuff that takes a few seconds

ResultClass slowTotal = asyncTask.Result;

问题在于它永远挂在最后一行。如果我打电话,它做同样的事情asyncTask.Wait()。如果我直接运行慢速SQL方法,则大约需要4秒钟。

我期望的行为是,当到达时asyncTask.Result,如果尚未完成,则应等待直到它到达,一旦返回,则应返回结果。

如果我逐步使用调试器,则SQL语句将完成并且lambda函数将完成,但是永远不会到达该return result;GetTotalAsync

知道我在做什么错吗?

对我需要调查以解决此问题的任何建议?

难道这是某个地方的僵局,如果有的话,有什么直接的方法可以找到它吗?


阅读 424

收藏
2020-05-19

共1个答案

一尘不染

是的,这是一个僵局。这是TPL的常见错误,所以不要感到难过。

当您编写await foo时,默认情况下,运行时将在该方法开始的同一SynchronizationContext上调度函数的继续。用英语来说,假设您是ExecuteAsync从UI线程调用的。您的查询在线程池线程上运行(因为您调用了Task.Run),但是随后等待结果。这意味着运行时将安排您的“
return result;”行在UI线程上运行,而不是将其安排回线程池。

那么,这种僵局又如何呢?假设您只有以下代码:

var task = dataSource.ExecuteAsync(_ => 42);
var result = task.Result;

因此,第一行开始了异步工作。然后第二行 阻塞UI线程
。因此,当运行时想要在UI线程上重新运行“返回结果”行时,它必须等到Result完成后才能执行。但是,当然,要等到返回发生后才能给出结果。僵局。

这说明了使用TPL的关键规则:在.ResultUI线程(或其他一些花哨的同步上下文)上使用时,必须小心确保不会将Task所依赖的任何项目安排到UI线程中。否则邪恶就会发生。

所以你会怎么做?选项#1随处可见,但是正如您所说的,这已经不是一个选择。您可以使用的第二种选择是停止使用await。您可以将两个函数重写为:

public static Task<T> ExecuteAsync<T>(this OurDBConn dataSource, Func<OurDBConn, T> function)
{
    string connectionString = dataSource.ConnectionString;

    // Start the SQL and pass back to the caller until finished
    return Task.Run(
        () =>
        {
            // Copy the SQL connection so that we don't get two commands running at the same time on the same open connection
            using (var ds = new OurDBConn(connectionString))
            {
                return function(ds);
            }
        });
}

public static Task<ResultClass> GetTotalAsync( ... )
{
    return this.DBConnection.ExecuteAsync<ResultClass>(
        ds => ds.Execute("select slow running data into result"));
}

有什么不同?现在没有等待的地方,因此没有任何隐式调度到UI线程的事情。对于像这样的简单方法,它们只有一个返回值,所以没有必要做一个“ var result = await...; return result”模式。只需删除异步修改器,然后直接传递任务对象即可。如果没有其他问题,那么开销会更少。

选项#3指定您不希望将等待调度回UI线程,而只是调度到线程池。您可以使用ConfigureAwait方法执行此操作,如下所示:

public static async Task<ResultClass> GetTotalAsync( ... )
{
    var resultTask = this.DBConnection.ExecuteAsync<ResultClass>(
        ds => return ds.Execute("select slow running data into result");

    return await resultTask.ConfigureAwait(false);
}

通常,等待任务将安排在UI线程上;等待的结果ContinueAwait将忽略您所处的任何上下文,并始终计划到线程池中。这样做的缺点是您必须在.Result所依赖的所有函数中的
任何地方 都撒上它,因为任何遗漏都.ConfigureAwait可能导致另一个死锁。

2020-05-19