为什么这个asynchronous操作挂起?

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

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

然后我有一个中级别的asynchronous方法,调用这个来得到一些缓慢的总计:

 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 ,如果它没有完成,它应该等到它,一旦它是应该返回结果。

如果我使用debugging器完成SQL语句,lambda函数完成,但return result; GetTotalAsync行永远不会到达。

任何想法我做错了什么?

任何build议,我需要调查,以解决这个问题?

这可能是一个僵局,如果有的话,是否有直接的方法来find它?

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

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

那么这个僵局如何呢? 想象一下你只是有这样的代码:

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

所以第一行开始了asynchronous工作。 第二行然后阻塞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 ”模式没有意义, 只要删除asynchronous修改器,并直接传递任务对象。 如果没有其他的东西,那么开销就会小一点

选项#3是指定你不希望你的等待时间安排回UI线程,而只是安排到UI线程。 您可以使用ConfigureAwait方法执行此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可能是另一个僵局的原因。

这是我在博客上描述的经典混合async死锁场景。 Jason描述得很好:默认情况下,每await就会保存一个“上下文”,并用于继续async方法。 这个“上下文”是当前的SynchronizationContext除非它是null ,在这种情况下它是当前的TaskScheduler 。 当async方法试图继续时,它首先重新input捕获的“上下文”(在这种情况下,一个ASP.NET SynchronizationContext上下文)。 ASP.NET SynchronizationContext每次只允许上下文中的一个线程,并且上下文中已经有一个线程 – 在Task.Result阻塞的线程。

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

  1. 一直使用async 。 你提到你“不能”这样做,但我不知道为什么不。 .NET 4.5上的ASP.NET MVC当然可以支持async操作,这不是一个困难的变化。
  2. 尽可能使用ConfigureAwait(continueOnCapturedContext: false) 。 这覆盖了在捕获的上下文中恢复的默认行为。

我处于同样的死锁状态,但在我的情况下,从同步方法调用一个asynchronous方法,对我来说是什么是:

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

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

只是添加到被接受的答案(没有足够的代表评论),我有这个问题出现阻塞使用task.Result ,事件虽然每一个下面有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); } 

这个问题实际上与外部库代码有关。 asynchronous库方法试图继续在调用同步上下文,无论我如何configurationawait,导致死锁。

因此,答案是推出我自己版本的外部库代码ExternalLibraryStringAsync ,以便它具有所需的延续属性。


为历史目的错误的答案

在经历了很多痛苦和痛苦之后,我find了埋在这篇博文中的解决scheme(Ctrl-f for'deadlock')。 它围绕使用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); }