为什么此异步操作挂起?


102

我有一个多层.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

知道我在做什么错吗?

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

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

Answers:


150

是的,这是一个僵局。这是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可能导致另一个死锁。


6
顺便说一句,问题是关于ASP.NET的,因此没有UI线程。但是由于ASP.NET,死锁的问题完全相同SynchronizationContext
svick

这解释了很多,因为我有类似的.Net 4代码,虽然没有问题,但使用了不带async/ await关键字的TPL 。
基思(Keith)2013年


如果有人在寻找VB.net代码(例如我),请在此处进行解释:docs.microsoft.com/zh-cn/dotnet/visual-basic/programming-guide/…–
MichaelDarkBlue


36

正如我在博客中所描述的async,这是经典的混合死锁场景。Jason对此进行了很好的描述:默认情况下,每个处都会保存一个“上下文”,并用于继续该方法。除非它是当前的,否则它是当前的,在这种情况下,它是当前的。当方法尝试继续时,它首先重新输入捕获的“上下文”(在本例中为ASP.NET )。ASP.NET一次只能在上下文中允许一个线程,并且上下文中已经存在一个线程-该线程被阻塞。awaitasyncSynchronizationContextnullTaskSchedulerasyncSynchronizationContextSynchronizationContextTask.Result

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

  1. 一直使用async下去。您提到您“不能”这样做,但是我不确定为什么不这样做。.NET 4.5上的ASP.NET MVC当然可以支持async操作,并且进行更改并非难事。
  2. 使用ConfigureAwait(continueOnCapturedContext: false)尽可能多地。这将覆盖在捕获的上下文上恢复的默认行为。

是否ConfigureAwait(false)保证当前功能在不同的上下文中恢复?
chu x

MVC框架支持它,但这是现有MVC应用程序的一部分,其中已经存在许多客户端JS。在async不中断客户端工作方式的情况下,我无法轻松地切换到某个操作。我当然计划较长期地研究这种选择。
Keith

只是为了澄清我的意见-我很好奇,使用ConfigureAwait(false)完调用树是否可以解决OP的问题。
chu x

3
@Keith:进行MVC操作async完全不会影响客户端。我将在另一篇博客async“不更改HTTP协议”中对此进行解释。
Stephen Cleary 2013年

1
@Keith:async通过代码库“增长” 是正常的。如果您的控制器方法可能依赖于异步操作,则基类方法返回Task<ActionResult>。将大型项目过渡到async总是很async麻烦,因为混合和同步代码既困难又棘手。纯async代码要简单得多。
Stephen Cleary

12

我处于同样的僵局情况,但从同步方法调用异步方法时,对我有用的是:

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

这是个好方法吗?


该解决方案也对我有用,但是我不确定这是否是一个好的解决方案,或者它可能会折断。任何人都可以解释一下
Konstantin Vdovkin

好吧,最后我采用了该解决方案,它可以在生产环境中正常工作……..
Danilow

1
我认为您正在使用Task.Run降低性能。在我的测试中,Task.Run将100ms http请求的执行时间几乎增加了一倍。
蒂莫西·冈萨雷斯

1
这很有意义,您正在创建一个用于包装异步调用的新任务,性能是一种折衷方案
Danilow

太神奇了,这对我也起作用,我的情况也是由调用异步方法的同步方法引起的。谢谢!
莱昂纳多·斯皮纳

4

只是为了增加接受的答案(没有足够的评论代表),我阻止了使用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);
}

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

因此,答案是滚动我自己的外部库代码版本ExternalLibraryStringAsync,以使其具有所需的延续属性。


历史目的的错误答案

经过一番痛苦和痛苦,我发现解决方案隐藏在此博客文章中(Ctrl-f表示“死锁”)。它围绕使用task.ContinueWith而不是光秃秃的task.Result

先前陷入僵局的示例:

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);
}

什么是反对票?此解决方案为我工作。
卡梅隆·杰弗斯

您要Task在完成之前返回对象,并且无法为调用方提供确定何时实际发生返回对象的突变的方法。
–Servy

嗯,我明白了。因此,我应该公开一种使用手动阻塞while循环(或类似方法)的“等待任务完成”方法吗?还是将这种块包装到GetFooSynchronous方法中?
卡梅伦·杰弗斯

1
如果这样做,它将陷入僵局。您需要通过返回a Task而不是阻塞来完全异步。
–Servy

不幸的是,这不是一个选择,该类实现了一个我无法更改的同步接口。
卡梅伦·杰弗斯

0

快速解答:更改此行

ResultClass slowTotal = asyncTask.Result;

ResultClass slowTotal = await asyncTask;

为什么?您不应该使用.result来获取大多数应用程序(控制台应用程序除外)内部任务的结果,如果这样做,则程序到达那里时将挂起

如果您想使用.Result,也可以尝试以下代码

ResultClass slowTotal = Task.Run(async ()=>await asyncTask).Result;
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.