C# 使用ContinueWith或Async Wait时的不同行为
当我在HttpClient调用中使用async await方法(如下例)时,这段代码会导致死锁。将异步等待方法替换为C# 使用ContinueWith或Async Wait时的不同行为,c#,.net,asp.net-mvc,multithreading,async-await,C#,.net,Asp.net Mvc,Multithreading,Async Await,当我在HttpClient调用中使用async await方法(如下例)时,这段代码会导致死锁。将异步等待方法替换为t.ContinueWith,它可以正常工作。为什么? public class MyFilter: ActionFilterAttribute { public override void OnActionExecuting(ActionExecutingContext filterContext) { var user = _authService.G
t.ContinueWith
,它可以正常工作。为什么?
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);
}
}
公共类MyFilter:ActionFilterAttribute{
公共覆盖无效OnActionExecuting(ActionExecutingContext filterContext){
var user=\u authService.GetUserAsync(username).Result;
}
}
公共类AuthService:IAuthService{
公共异步任务GetUserAsync(字符串用户名){
var jsonUsr=await\u httpClientWrp.GetStringAsync(url).ConfigureAwait(false);
return wait JsonConvert.DeserializeObjectAsync(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;
}
}
公共类HttpClientWrapper:IHttpClient{
公共任务GetStringAsync(字符串url){
return\u client.GetStringAsync(url).ContinueWith(t=>{
_InfoFormat(“响应:{0}”,url);
返回t.Result;
});
}
此代码将死锁:
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;
}
}
公共类HttpClientWrapper:IHttpClient{
公共异步任务GetStringAsync(字符串url){
string result=await\u client.GetStringAsync(url);
_InfoFormat(“响应:{0}”,url);
返回结果;
}
}
当然,我的答案只是部分,但我还是会继续回答
您的任务。继续(…)
调用没有指定调度程序,因此TaskScheduler。将使用Current
,无论当时是什么。但是,当等待的任务完成时,您的wait
代码片段将在捕获的上下文上运行,因此这两位代码可能会或可能不会产生类似的行为,具体取决于TaskSch的值eduler.Current
例如,如果直接从UI代码调用第一个代码段(在这种情况下,TaskScheduler.Current==TaskScheduler.Default
),则将在默认的TaskScheduler
上执行延续(日志代码)-即在线程池上执行
但是,在第二个代码段中,不管您是否对GetStringAsync返回的任务使用ConfigureAwait(false)
,延续(日志记录)实际上都将在UI线程上运行。ConfigureAwait(false)
只会在调用GetStringAsync
后影响代码的执行
这里还有一些东西可以说明这一点:
private async void Form1_Load(object sender, EventArgs e)
{
await this.Blah().ConfigureAwait(false);
// InvalidOperationException here.
this.Text = "Oh noes, I'm no longer on the UI thread.";
}
private async Task Blah()
{
await Task.Delay(1000);
this.Text = "Hi, I'm on the UI thread.";
}
给定的代码将Blah()中的文本设置得很好,但它在加载处理程序的continuation中引发了一个跨线程异常。关于等待死锁原因的解释
您的第一行:
var user = _authService.GetUserAsync(username).Result;
在线程等待GetUserAsync
的结果时阻止该线程和当前上下文
当使用wait
时,它会在等待的任务完成后尝试在原始上下文上运行任何剩余语句,如果原始上下文被阻止(这是因为.Result
),则会导致死锁。看起来您试图通过使用.configurewait(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
当\u client.GetStringAsync
完成时,由于该上下文被阻止,其余代码无法在原始上下文上继续
为什么ContinueWith表现不同
ContinueWith
不会尝试在原始上下文上运行另一个块(除非您使用附加参数告诉它),因此不会出现此问题
这就是你注意到的行为差异
一个异步的解决方案
如果仍要使用async
而不是ContinueWith
,则可以将.configurewait(false)
添加到遇到的第一个async
:
string result = await _client.GetStringAsync(url).ConfigureAwait(false);
您很可能已经知道,它告诉wait
不要尝试在原始上下文中运行剩余的代码
未来的注意事项
如果可能,在使用async/await时尽量不要使用阻塞方法。请参阅以了解将来如何避免这种情况。我将介绍这种死锁行为和方法
- 默认情况下,
将安排其继续在当前的await
内运行,或者(如果没有SynchronizationContext
)当前的SynchronizationContext
(在这种情况下是ASP.NET请求TaskScheduler
)内运行SynchronizationContext
- ASP.NET
表示请求上下文,ASP.NET一次只允许该上下文中的一个线程SynchronizationContext
SynchronizationContext
以运行InfoFormat
。但是,SynchronizationContext
中已经有一个线程-在Result
上被阻止的线程(等待async
方法完成)
另一方面,默认情况下,ContinueWith
的默认行为将其继续调度到当前的TaskScheduler
(在本例中是线程池TaskScheduler
)
正如其他人所指出的,最好使用wait
“一路”,即不要阻塞async
代码。不幸的是,在这种情况下,这不是一个选项,因为(请附带说明)
因此,您可以选择使用ConfigureAwait(false)
或仅使用同步方法。在这种情况下,我建议使用同步方法。ConfigureAwait(false)
仅在应用它的任务
尚未完成时有效,因此我建议使用ConfigureAwait(false)后
,您应该永远使用它