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

Kei*_*ith 96 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;
}
Run Code Online (Sandbox Code Playgroud)

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

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

    return result;
}
Run Code Online (Sandbox Code Playgroud)

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

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

// do other stuff that takes a few seconds

ResultClass slowTotal = asyncTask.Result;
Run Code Online (Sandbox Code Playgroud)

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

我期待的行为是,当它到达时asyncTask.Result,如果它没有完成它应该等到它,并且一旦它它应该返回结果.

如果我有一个调试器步骤通过SQL语句完成和lambda函数完成,但return result;GetTotalAsync则永远无法实现.

知道我做错了什么吗?

有什么建议我需要调查以解决这个问题吗?

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

Jas*_*ski 141

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

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

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

var task = dataSource.ExecuteAsync(_ => 42);
var result = task.Result;
Run Code Online (Sandbox Code Playgroud)

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

这说明了使用TPL的关键规则:当您.Result在UI线程(或其他一些花哨的同步上下文)上使用时,您必须小心确保任务所依赖的任何内容都没有安排到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"));
}
Run Code Online (Sandbox Code Playgroud)

有什么不同?现在没有任何等待,所以没有任何隐式安排到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);
}
Run Code Online (Sandbox Code Playgroud)

等待任务通常会安排到UI线程,如果你在它上面; 等待结果ContinueAwait将忽略您所处的任何上下文,并始终安排到线程池.这样做的缺点是你必须在你的.Result所依赖的所有函数中随处可见,因为任何错过的.ConfigureAwait可能是另一个死锁的原因.

  • 顺便说一下,问题是关于ASP.NET,所以没有UI线程.但由于ASP.NET`SynchronizationContext`,死锁的问题完全相同. (6认同)
  • TPL =任务并行库https://msdn.microsoft.com/en-us/library/dd460717(v=vs.110).aspx (2认同)

Ste*_*ary 34

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

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

  1. 一直使用async.你提到你"不能"这样做,但我不确定为什么不这样做..NET 4.5上的ASP.NET MVC当然可以支持async操作,并且它不是一个很难做出的改变.
  2. 使用ConfigureAwait(continueOnCapturedContext: false)尽可能多地.这将覆盖在捕获的上下文中恢复的默认行为.

  • @Keith:制作MVC动作`async`根本不会影响客户端.我在另一篇博文中解释了这一点,[`async`不改变HTTP协议](http://nitoprograms.blogspot.com/2012/08/async-doesnt-change-http-protocol.html). (3认同)

Dan*_*low 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
}
Run Code Online (Sandbox Code Playgroud)

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


Cam*_*ers 5

只是为了添加到已接受的答案(没有足够的代表来评论),我在阻止使用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);
}
Run Code Online (Sandbox Code Playgroud)

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

因此,答案是滚动我自己的外部库代码版本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);
}
Run Code Online (Sandbox Code Playgroud)

避免这样的僵局:

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);
}
Run Code Online (Sandbox Code Playgroud)