【问题标题】:Different behavior when using ContinueWith or Async-Await使用 ContinueWith 或 Async-Await 时的不同行为
【发布时间】:2013-06-04 11:40:12
【问题描述】:

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

【问题讨论】:

    标签: c# .net asp.net-mvc multithreading async-await


    【解决方案1】:

    解释为什么你的等待死锁

    你的第一行:

     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 时尽量不使用阻塞方法。请参阅Preventing a deadlock when calling an async method without using await 以在将来避免这种情况。

    【讨论】:

    • “它试图在原始线程上运行任何剩余的语句”这并不完全正确,它试图在原始 context 上运行它们。在 ASP.NET 中,上下文不绑定到特定线程,因此延续可以在不同的线程上执行。但是你仍然会遇到死锁,因为一次只能有一个线程在上下文中。
    【解决方案2】:

    我发现这里发布的其他解决方案在 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;
    }
    

    }

    【讨论】:

      【解决方案3】:

      我描述了这种死锁行为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 会产生维护负担。同步方法会更清晰。

      【讨论】:

        【解决方案4】:

        当然,我的回答只是部分,但我还是会继续。

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

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

        然而,在第二个 sn-p 中,无论您是否在 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() 中设置 Text 很好,但它在 Load 处理程序的延续内引发了跨线程异常。

        【讨论】:

        • await 主要查看SynchronizationContext.Current,而不是TaskScheduler.Current
        猜你喜欢
        • 2017-09-18
        • 1970-01-01
        • 2012-02-04
        • 1970-01-01
        • 1970-01-01
        • 2015-09-09
        • 1970-01-01
        • 1970-01-01
        • 2020-02-26
        相关资源
        最近更新 更多