我有一个多层.Net 4.5应用程序使用C#的新async
和await
关键字调用方法,这些关键字只是挂起而且我看不清楚原因。
在底部,我有一个异步方法,它扩展了我们的数据库实用程序OurDBConn
(基本上是底层DBConnection
和DBCommand
对象的包装器:
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
行。
知道我做错了吗?
对于我需要调查以解决此问题的任何建议?
这可能是某个地方的僵局,如果是这样,有没有找到它的直接方法?
答案 0 :(得分:134)
当您编写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
上阻塞的线程。
有两条准则可以避免这种僵局:
SynchronizationContext
。你提到你“不能”这样做,但我不确定为什么不这样做。 .NET 4.5上的ASP.NET MVC当然可以支持Task.Result
个动作,并且它不是一个很难做的改变。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;