114

我有一个多层 .Net 4.5 应用程序使用 C# 的 newasyncawait关键字调用一个方法,该方法只是挂起,我不明白为什么。

在底部,我有一个扩展我们的数据库实用程序的异步方法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

知道我做错了什么吗?

关于我需要在哪里进行调查以解决此问题的任何建议?

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

4

5 回答 5

161

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

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

那么这个僵局是怎么发生的呢?想象一下,您只有以下代码:

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

所以第一行开始了异步工作。然后第二行阻塞了 UI 线程。因此,当运行时想要在 UI 线程上运行“返回结果”行时,它不能在Result完成之前执行此操作。但是当然,在返回发生之前不能给出结果。僵局。

这说明了使用 TPL 的一个关键规则:当您.Result在 UI 线程(或其他一些花哨的同步上下文)上使用时,您必须小心确保 Task 所依赖的任何内容都不会被调度到 UI 线程。否则邪恶就会发生。

所以你会怎么做?选项 #1 在任何地方都使用 await ,但正如您所说,这已经不是一个选项。可供您使用的第二个选项是简单地停止使用 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" 模式是没有意义的;只需删除 async 修饰符并直接传递任务对象即可。如果没有别的,它的开销会更少。

选项 #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使用它,因为任何遗漏都可能导致另一个死锁。

于 2013-01-25T17:27:56.163 回答
40

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

有两个指导方针可以避免这种僵局:

  1. 一直使用async下去。你提到你“不能”这样做,但我不知道为什么不这样做。.NET 4.5 上的 ASP.NET MVC 当然可以支持async动作,而且做起来也不难。
  2. ConfigureAwait(continueOnCapturedContext: false)尽可能多地使用。这会覆盖在捕获的上下文上恢复的默认行为。
于 2013-01-25T18:30:28.733 回答
15

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

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

这是一个好方法,有什么想法吗?

于 2016-11-13T00:57:30.077 回答
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);
}

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

因此,答案是推出我自己的外部库代码版本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);
}
于 2015-12-22T19:36:43.097 回答
1

快速回答:更改此行

ResultClass slowTotal = asyncTask.Result;

ResultClass slowTotal = await asyncTask;

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

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

ResultClass slowTotal = Task.Run(async ()=>await asyncTask).Result;
于 2019-12-01T07:06:25.923 回答