使用ContinueWith或Async-Await时的不同行为

当我在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 GetUserAsync (string username) { var jsonUsr = await _httpClientWrp.GetStringAsync(url).ConfigureAwait(false); return await JsonConvert.DeserializeObjectAsync(jsonUsr); } } 

这有效:

 public class HttpClientWrapper : IHttpClient { public Task GetStringAsync(string url) { return _client.GetStringAsync(url).ContinueWith(t => { _log.InfoFormat("Response: {0}", url); return t.Result; }); } 

此代码将死锁:

 public class HttpClientWrapper : IHttpClient { public async Task GetStringAsync(string url) { string result = await _client.GetStringAsync(url); _log.InfoFormat("Response: {0}", url); return result; } } 

我在我的博客和最近的MSDN文章中描述了这种死锁行为。

  • await将默认安排其继续在当前SynchronizationContext内运行,或者(如果没有SynchronizationContext )当前TaskScheduler 。 (在本例中是ASP.NET请求SynchronizationContext )。
  • ASP.NET SynchronizationContext表示请求上下文,ASP.NET一次只允许该上下文中的一个线程。

因此,当HTTP请求完成时,它会尝试输入SynchronizationContext以运行InfoFormat 。 但是, SynchronizationContext已经存在一个线程 – 在Result阻塞的线程(等待async方法完成)。

另一方面,默认情况下, ContinueWith的默认行为将调度其继续当前的TaskScheduler (在本例中是线程池TaskScheduler )。

正如其他人所指出的那样,最好使用await “一直”,即不要阻止async代码。 不幸的是,在这种情况下,这不是一个选项,因为MVC不支持异步操作filter (作为附注,请在此投票支持此支持 )。

因此,您可以选择使用ConfigureAwait(false)或仅使用同步方法。 在这种情况下,我建议使用同步方法。 ConfigureAwait(false)仅在其应用的Task尚未完成时才有效,所以我建议一旦你使用ConfigureAwait(false) ,你应该在该点之后的方法中为每个await使用它(在这种情况下,每个调用堆栈中的方法)。 如果出于效率原因正在使用ConfigureAwait(false) ,那么这很好(因为它在技术上是可选的)。 在这种情况下,出于正确原因,必须使用ConfigureAwait(false) ,因此IMO会产生维护负担。 同步方法会更清晰。

解释你为何等待死锁的原因

你的第一行:

  var user = _authService.GetUserAsync(username).Result; 

在等待GetUserAsync的结果时阻塞该线程和当前上下文。

当使用await时,它会在完成任务等待后尝试在原始上下文中运行任何剩余的语句,如果原始上下文被阻塞(这是因为.Result ),则会导致死锁。 看起来您试图通过在GetUserAsync使用.ConfigureAwait(false)来抢占此问题,但是当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表现不同

ContinueWith不会尝试在原始上下文中运行其他块(除非您使用其他参数告诉它),因此不会遇到此问题。

这是您注意到的行为差异。

一个异步的解决方案

如果您仍想使用async而不是ContinueWith ,则可以将.ConfigureAwait(false)添加到第一个遇到的async

 string result = await _client.GetStringAsync(url).ConfigureAwait(false); 

您可能已经知道的,告诉await不要尝试在原始上下文中运行剩余的代码。

注意将来

尽可能尝试在使用async / await时不使用阻塞方法。 请参阅调用异步方法时防止死锁,而不使用await以避免将来出现此问题。

当然,我的回答只是部分,但无论如何我都会继续。

您的Task.ContinueWith(...)调用未指定调度程序,因此将使用TaskScheduler.Current – 无论当时是什么。 但是, await片段将在等待任务完成时在捕获的上下文上运行,因此两位代码可能会也可能不会产生类似的行为 – 具体取决于TaskScheduler.Current的值。

例如,如果直接从UI代码调用您的第一个代码段(在这种情况下, TaskScheduler.Current == TaskScheduler.Default ,则继续(日志记录代码)将在默认的TaskScheduler上执行 – 即在线程池上执行。

但是,在第二个片段中,无论是否对GetStringAsync返回的任务使用ConfigureAwait(false) ,继续(日志记录)实际上都将在UI线程上运行。 等待对GetStringAsync的调用之后ConfigureAwait(false)将仅影响代码的执行。

这是另一个说明这一点:

  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()中的Text设置得很好,但它会在Load处理程序的continuation中抛出一个跨线程exception。

我发现这里发布的其他解决方案在ASP .NET MVC 5上没有用,它仍然使用同步动作filter。 发布的解决方案不保证将使用新线程,它们只是指定不必使用相同的线程。

我的解决方案是使用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; } 

}