为什么这个异步操作会挂起?

时间:2013-01-25 16:44:45

标签: c# asynchronous task-parallel-library async-await c#-5.0

我有一个多层.Net 4.5应用程序使用C#的新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行。

知道我做错了吗?

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

这可能是某个地方的僵局,如果是这样,有没有找到它的直接方法?

5 个答案:

答案 0 :(得分:134)

是的,这可能是一个僵局。和TPL一样的常见错误,所以不要感觉不好。

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

那么这种僵局怎么样?想象一下,你只需要这段代码:

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

所以第一行开始了异步工作。第二行然后阻止UI线程。因此,当运行时希望在UI线程上运行“返回结果”行时,在Result完成之前,它不能这样做。但是,当然,在返回发生之前不能给出结果。死锁。

这说明了使用TPL的关键规则:当您在UI线程(或其他一些花哨的同步上下文)上使用.Result时,您必须小心确保任务所依赖的任何内容都不会被安排到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线程,而只是安排到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都可能是另一个死锁的原因。

答案 1 :(得分:34)

这是经典的混合 - async死锁场景,as I describe on my blog。杰森很好地描述了它:默认情况下,每个await都会保存一个“上下文”并用于继续async方法。除非是SynchronizationContext,否当null方法尝试继续时,它首先重新进入捕获的“上下文”(在本例中为ASP.NET TaskScheduler)。 ASP.NET async一次只允许上下文中的一个线程,并且上下文中已经存在一个线程 - SynchronizationContext上阻塞的线程。

有两条准则可以避免这种僵局:

  1. 一直使用SynchronizationContext。你提到你“不能”这样做,但我不确定为什么不这样做。 .NET 4.5上的ASP.NET MVC当然可以支持Task.Result个动作,并且它不是一个很难做的改变。
  2. 尽可能使用async。这将覆盖在捕获的上下文中恢复的默认行为。

答案 2 :(得分:11)

我遇到了同样的死锁情况,但在我的情况下,从同步方法调用异步方法,对我有用的是:

private static SiteMetadataCacheItem GetCachedItem()
{
      TenantService TS = new TenantService(); // my service datacontext
      var CachedItem = Task.Run(async ()=> 
               await TS.GetTenantDataAsync(TenantIdValue)
      ).Result; // dont deadlock anymore
}

这是一个好方法,任何想法?

答案 3 :(得分:3)

只是为了添加到已接受的答案(没有足够的评论代表),我在阻止使用task.Result事件时出现此问题,但事件虽然await下面的ConfigureAwait(false) public Foo GetFooSynchronous() { var foo = new Foo(); foo.Info = GetInfoAsync.Result; // often deadlocks in ASP.NET return foo; } private async Task<string> GetInfoAsync() { return await ExternalLibraryStringAsync().ConfigureAwait(false); } ExternalLibraryStringAsync,在这个例子中:

task.ContinueWith

问题实际上在于外部库代码。无论我如何配置await,异步库方法都试图在调用同步上下文中继续,导致死锁。

因此,答案是滚动我自己的外部库代码task.Result版本,以便它具有所需的延续属性。

<小时/> 出于历史目的的错误答案

经过多次痛苦和痛苦之后,我找到了解决方案buried in this blog post(Ctrl-f for&#39; deadlock&#39;)。它围绕着使用public Foo GetFooSynchronous() { var foo = new Foo(); foo.Info = GetInfoAsync.Result; // often deadlocks in ASP.NET return foo; } private async Task<string> GetInfoAsync() { return await ExternalLibraryStringAsync().ConfigureAwait(false); } ,而不是裸public Foo GetFooSynchronous { var foo = new Foo(); GetInfoAsync() // ContinueWith doesn't run until the task is complete .ContinueWith(task => foo.Info = task.Result); return foo; } private async Task<string> GetInfoAsync { return await ExternalLibraryStringAsync().ConfigureAwait(false); }

以前死锁的例子:

b:= make([]int, 0, 5) // length: 0, cap: 5

避免这样的死锁:

c:= b[:2]  // length: 2 (?), cap: 5

答案 4 :(得分:-1)

快速解答: 更改此行

ResultClass slowTotal = asyncTask.Result;

ResultClass slowTotal = await asyncTask;

为什么?您不应该使用.result来获取除控制台应用程序之外的大多数应用程序中的任务结果,否则您的程序将在到达那里时挂起

如果要使用.Result,也可以尝试下面的代码

ResultClass slowTotal = Task.Run(async ()=>await asyncTask).Result;