当我在HttpClient调用中使用async-await方法(如下例所示)时,此代码会导致死锁。用t.ContinueWith
替换async-await方法,它可以正常工作。为什么呢?
public class MyFilter: ActionFilterAttribute {
public override void OnActionExecuting(ActionExecutingContext filterContext) {
var user = _authService.GetUserAsync(username).Result;
}
}
public class AuthService: IAuthService {
public async Task<User> GetUserAsync (string username) {
var jsonUsr = await _httpClientWrp.GetStringAsync(url).ConfigureAwait(false);
return await JsonConvert.DeserializeObjectAsync<User>(jsonUsr);
}
}
这有效:
public class HttpClientWrapper : IHttpClient {
public Task<string> GetStringAsync(string url) {
return _client.GetStringAsync(url).ContinueWith(t => {
_log.InfoFormat("Response: {0}", url);
return t.Result;
});
}
此代码将死锁:
public class HttpClientWrapper : IHttpClient {
public async Task<string> GetStringAsync(string url) {
string result = await _client.GetStringAsync(url);
_log.InfoFormat("Response: {0}", url);
return result;
}
}
答案 0 :(得分:9)
我描述了这种死锁行为on my blog和in a recent MSDN article。
await
会将其续约计划在当前SynchronizationContext
内,或者(如果没有SynchronizationContext
)当前TaskScheduler
。 (在本例中是ASP.NET请求SynchronizationContext
)。SynchronizationContext
表示请求上下文,ASP.NET一次只允许该上下文中的一个线程。因此,当HTTP请求完成时,它会尝试输入SynchronizationContext
以运行InfoFormat
。但是,SynchronizationContext
中已存在一个帖子 - Result
上阻止的帖子(等待async
方法完成)。
另一方面,默认情况下ContinueWith
的默认行为会将其继续安排到当前TaskScheduler
(在这种情况下是线程池TaskScheduler
)。
正如其他人所说,最好一直使用await
“,即不要阻止async
代码。不幸的是,自MVC does not support asynchronous action filters以来,这不是一个选项(请注意,请vote for this support here)。
因此,您可以选择使用ConfigureAwait(false)
或仅使用同步方法。在这种情况下,我建议使用同步方法。 ConfigureAwait(false)
仅在申请的Task
尚未完成时才有效,因此我建议您使用ConfigureAwait(false)
之后,应该在方法中的每个await
使用它那一点(在这种情况下,在调用堆栈中的每个方法中)。如果出于效率原因使用ConfigureAwait(false)
,那就没问题(因为它在技术上是可选的)。在这种情况下,出于正确原因,ConfigureAwait(false)
是必要的,因此IMO会产生维护负担。同步方法会更清晰。
答案 1 :(得分:7)
你的第一行:
var user = _authService.GetUserAsync(username).Result;
在等待GetUserAsync
的结果时阻塞该线程和当前上下文。
当使用await
时,它会在完成任务等待后尝试在原始上下文中运行任何剩余的语句,如果原始上下文被阻止(这是因为{{1},则会导致死锁) })。您似乎尝试使用.Result
中的.ConfigureAwait(false)
来抢占此问题,但是当GetUserAsync
生效时,为时已晚,因为首先遇到另一个await
。实际执行路径如下所示:
await
当_authService.GetUserAsync(username)
_httpClientWrp.GetStringAsync(url) // not actually awaiting yet (it needs a result before it can be awaited)
await _client.GetStringAsync(url) // here's the first await that takes effect
完成时,其余代码无法在原始上下文中继续,因为该上下文被阻止。
_client.GetStringAsync
不会尝试在原始上下文中运行其他块(除非您使用其他参数告诉它),因此不会遇到此问题。
这是您注意到的行为差异。
如果您仍想使用ContinueWith
代替async
,可以将ContinueWith
添加到遇到的第一个.ConfigureAwait(false)
:
async
您可能已经知道的告诉string result = await _client.GetStringAsync(url).ConfigureAwait(false);
不要尝试在原始上下文中运行剩余的代码。
尽可能尝试在使用async / await时不使用阻塞方法。请参阅Preventing a deadlock when calling an async method without using await以避免将来发生此事。
答案 2 :(得分:2)
当然,我的回答只是部分,但无论如何我都会继续。
您的Task.ContinueWith(...)
调用未指定调度程序,因此将使用TaskScheduler.Current
- 无论当时是什么。但是,当等待任务完成时,您的await
代码段将在捕获的上下文上运行,因此代码的两位代码可能会或可能不会产生类似的行为 - 具体取决于{{{{1}的值1}}。
如果您的第一个代码段直接从UI代码调用(在这种情况下为TaskScheduler.Current
),则继续(日志记录代码)将在默认的TaskScheduler.Current == TaskScheduler.Default
上执行 - 也就是说,在线程上池。
但是,在第二个片段中,无论是否对GetStringAsync返回的任务使用TaskScheduler
,继续(日志记录)实际上都会在UI线程上运行。在{/ 1>等待ConfigureAwait(false)
的调用之后,ConfigureAwait(false)
只会影响代码的执行。
以下是其他事情来说明这一点:
GetStringAsync
给定的代码将Blah()中的Text设置得很好,但它会在Load处理程序的continuation中抛出一个跨线程异常。
答案 3 :(得分:0)
我发现这里发布的其他解决方案在ASP .NET MVC 5上不起作用,它仍然使用同步动作过滤器。发布的解决方案不保证将使用新线程,它们只是指定不必使用相同的线程。
我的解决方案是使用Task.Factory.StartNew()并在方法调用中指定TaskCreationOptions.LongRunning。这样可以确保始终使用新的/不同的线程,因此您可以放心,永远不会陷入僵局。
因此,使用OP示例,以下是适用于我的解决方案:
public class MyFilter: ActionFilterAttribute {
public override void OnActionExecuting(ActionExecutingContext filterContext) {
// var user = _authService.GetUserAsync(username).Result;
// Guarantees a new/different thread will be used to make the enclosed action
// avoiding deadlocking the calling thread
var user = Task.Factory.StartNew(
() => _authService.GetUserAsync(username).Result,
TaskCreationOptions.LongRunning).Result;
}
}