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

时间:2013-06-08 05:23:24

标签: c# .net asp.net-mvc multithreading 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<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;
    }
}

4 个答案:

答案 0 :(得分:9)

我描述了这种死锁行为on my blogin a recent MSDN article

    默认情况下,
  • await会将其续约计划在当前SynchronizationContext内,或者(如果没有SynchronizationContext)当前TaskScheduler。 (在本例中是ASP.NET请求SynchronizationContext)。
  • ASP.NET 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 完成时,其余代码无法在原始上下文中继续,因为该上下文被阻止。

为什么ContinueWith表现不同

_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;
}

}