7

当我在 HttpClient 调用中使用 async-await 方法(如下例所示)时,此代码会导致deadlock。用 a 替换 async-await 方法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);
    }
}

这有效:

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

4 回答 4

9

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

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

因此,当 HTTP 请求完成时,它会尝试进入SynchronizationContextto run InfoFormat。但是,已经有一个线程在SynchronizationContext- 阻塞上Result(等待async方法完成)。

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

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

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

于 2013-06-08T13:46:07.493 回答
7

为什么你的等待死锁的解释

你的第一行:

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

在等待GetUserAsync.

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

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

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

异步解决方案

如果你仍然想使用async而不是ContinueWith,你可以.ConfigureAwait(false)在第一次遇到的时候添加async

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

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

未来的注意事项

尽可能在使用 async/await 时尝试不使用阻塞方法。请参阅在不使用 await 的情况下调用异步方法时防止死锁,以避免将来出现这种情况。

于 2013-06-08T06:02:17.787 回答
2

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

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

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

然而,在第二个片段中,无论您是否ConfigureAwait(false)在 GetStringAsync 返回的任务上使用,延续(日志记录)实际上都会在 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() 中设置了 Text 就好了,但是它在 Load 处理程序的延续内引发了一个跨线程异常。

于 2013-06-08T05:59:07.310 回答
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;
}

}

于 2013-12-29T00:56:15.963 回答