Different behavior when using ContinueWith or Async-Await

前端 未结 4 898
轻奢々
轻奢々 2020-12-28 10:28

When I use an async-await method (as the example below) in a HttpClient call, this code causes a deadlock. Replacing the async-await method with a t.C

相关标签:
4条回答
  • 2020-12-28 11:00

    An explanation on why your await deadlocks

    Your first line:

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

    blocks that thread and the current context while it waits for the result of GetUserAsync.

    When using await it attempts to run any remaining statements back on the original context after the task being waited on finishes, which causes deadlocks if the original context is blocked (which is is because of the .Result). It looks like you attempted to preempt this problem by using .ConfigureAwait(false) in GetUserAsync, however by the time that that await is in effect it's too late because another await is encountered first. The actual execution path looks like this:

    _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
    

    When _client.GetStringAsync finishes, the rest of the code can't continue on the original context because that context is blocked.

    Why ContinueWith behaves differently

    ContinueWith doesn't try to run the other block on the original context (unless you tell it to with an additional parameter) and thus does not suffer from this problem.

    This is the difference in behavior that you noticed.

    A solution with async

    If you still want to use async instead of ContinueWith, you can add the .ConfigureAwait(false) to the first encountered async:

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

    which as you most likely already know, tells await not to try to run the remaining code on the original context.

    Note for the future

    Whenever possible, attempt to not use blocking methods when using async/await. See Preventing a deadlock when calling an async method without using await for avoiding this in the future.

    0 讨论(0)
  • 2020-12-28 11:03

    I found the other solutions posted here did not work for me on ASP .NET MVC 5, which still uses synchronous Action Filters. The posted solutions don't guarantee a new thread will be used, they just specify that the same thread does not HAVE to be used.

    My solution is to use Task.Factory.StartNew() and specifying TaskCreationOptions.LongRunning in the method call. This ensures a new/different thread is always used, so you can be assured you will never get a deadlock.

    So, using the OP example, the following is the solution that works for me:

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

    }

    0 讨论(0)
  • 2020-12-28 11:07

    I describe this deadlock behavior on my blog and in a recent MSDN article.

    • await will by default schedule its continuation to run inside the current SynchronizationContext, or (if there is no SynchronizationContext) the current TaskScheduler. (Which in this case is the ASP.NET request SynchronizationContext).
    • The ASP.NET SynchronizationContext represents the request context, and ASP.NET only allows one thread in that context at a time.

    So, when the HTTP request completes, it attempts to enter the SynchronizationContext to run InfoFormat. However, there is already a thread in the SynchronizationContext - the one blocked on Result (waiting for the async method to complete).

    On the other hand, the default behavior for ContinueWith by default will schedule its continuation to the current TaskScheduler (which in this case is the thread pool TaskScheduler).

    As others have noted, it's best to use await "all the way", i.e., don't block on async code. Unfortunately, that's not an option in this case since MVC does not support asynchronous action filters (as a side note, please vote for this support here).

    So, your options are to use ConfigureAwait(false) or to just use synchronous methods. In this case, I recommend synchronous methods. ConfigureAwait(false) only works if the Task it's applied to has not already completed, so I recommend that once you use ConfigureAwait(false), you should use it for every await in the method after that point (and in this case, in each method in the call stack). If ConfigureAwait(false) is being used for efficiency reasons, then that's fine (because it's technically optional). In this case, ConfigureAwait(false) would be necessary for correctness reasons, so IMO it creates a maintenance burden. Synchronous methods would be clearer.

    0 讨论(0)
  • 2020-12-28 11:16

    Granted, my answer is only partial, but I'll go ahead with it anyway.

    Your Task.ContinueWith(...) call does not specify the scheduler, therefore TaskScheduler.Current will be used - whatever that is at the time. Your await snippet, however, will run on the captured context when the awaited task completes, so the two bits of code may or may not produce similar behaviour - depending on the value of TaskScheduler.Current.

    If, say, your first snippet is called from the UI code directly (in which case TaskScheduler.Current == TaskScheduler.Default, the continuation (logging code) will execute on the default TaskScheduler - that is, on the thread pool.

    In the second snippet, however, the continuation (logging) will actually run on the UI thread regardless of whether you use ConfigureAwait(false) on the task returned by GetStringAsync, or not. ConfigureAwait(false) will only affect the execution of the code after the call to GetStringAsync is awaited.

    Here's something else to illustrate this:

        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.";
        }
    

    The given code sets the Text within Blah() just fine, but it throws a cross-threading exception inside the continuation in the Load handler.

    0 讨论(0)
提交回复
热议问题