async/await deadlocking when using a Synchronizati

2019-03-11 09:00发布

According to this link:

When you are awaiting on a method with await keyword, compiler generates bunch of code in behalf of you. One of the purposes of this action is to handle synchronization with the UI thread. The key
component of this feature is the SynchronizationContext.Current which gets the synchronization context for the current thread.
SynchronizationContext.Current is populated depending on the
environment you are in. The GetAwaiter method of Task looks up for
SynchronizationContext.Current. If current synchronization context is not null, the continuation that gets passed to that awaiter will get posted back to that synchronization context.

When consuming a method, which uses the new asynchronous language features, in a blocking fashion, you will end up with a deadlock if
you have an available SynchronizationContext.
When you are consuming such methods in a blocking fashion (waiting on the Task with Wait method or taking the result directly from the Result property of the Task), you will block the main thread at the same time. When eventually the Task completes inside that method in the threadpool, it is going to invoke the continuation to post back to the main thread because SynchronizationContext.Current is available and captured. But there is a problem here: the UI thread is blocked and you have a deadlock!

    public class HomeController : Controller
    {    
        public ViewResult CarsSync() 
        {
            SampleAPIClient client = new SampleAPIClient();
            var cars = client.GetCarsInAWrongWayAsync().Result;
            return View("Index", model: cars);
        }
    }

    public class SampleAPIClient 
    {
        private const string ApiUri = "http://localhost:17257/api/cars";
        public async Task<IEnumerable<Car>> GetCarsInAWrongWayAsync()
        {
            using (var client = new HttpClient()) 
            {
                var response = await client.GetAsync(ApiUri);

                // Not the best way to handle it but will do the work for demo purposes
                response.EnsureSuccessStatusCode();
                return await response.Content.ReadAsAsync<IEnumerable<Car>>();
            }
        }
    }

I have trouble understanding the bolded part of the statement above, but when I test the code above, it deadlocks as expected. But I still can't understand why the UI thread is blocked?

In this case, what is the available SynchronizationContext? Is it the UI thread?

2条回答
beautiful°
2楼-- · 2019-03-11 09:28

The key point is that some SynchronizationContexts only allow a single thread to run code at the same time. One thread is calling Result or Wait. When the async methods wants to enter it can't.

Some SynchronizationContexts are mutli-threaded and the problem does not occur.

查看更多
甜甜的少女心
3楼-- · 2019-03-11 09:54

I explain this in full in my own blog post, but to reiterate here...

await by default will capture a current "context" and resume its async method on that context. This context is SynchronizationContext.Current unless it is null, in which case it is TaskScheduler.Current.

Deadlocks can occur when you have a one-thread-at-a-time SynchronizationContext and you block on a task representing asynchronous code (e.g., using Task.Wait or Task<T>.Result). Note that it is the blocking that causes the deadlock, not just the SynchronizationContext; the appropriate resolution (almost always) is to make the calling code asynchronous (e.g., replace Task.Wait/Task<T>.Result with await). This is especially true on ASP.NET.

But I still can't understand why the UI thread is blocked?

Your example is running on ASP.NET; there is no UI thread.

what is the available SynchronizationContext?

The current SynchronizationContext should be an instance of AspNetSynchronizationContext, a context that represents an ASP.NET request. This context only allows one thread in at a time.


So, walking through your example:

When a request comes in for this action, CarsSync will start executing within that request context. It proceeds to this line:

var cars = client.GetCarsInAWrongWayAsync().Result;

which is essentially the same as this:

Task<IEnumerable<Car>> carsTask = client.GetCarsInAWrongWayAsync();
var cars = carsTask.Result;

So, it proceeds to call into GetCarsInAWrongWayAsync, which runs until it hits its first await (the GetAsync call). At this point, GetCarsInAWrongWayAsync captures its current context (the ASP.NET request context) and returns an incomplete Task<IEnumerable<Car>>. When the GetAsync download finishes, GetCarsInAWrongWayAsync will resume executing on that ASP.NET request context and (eventually) complete the task it already returned.

However, as soon as GetCarsInAWrongWayAsync returns the incomplete task, CarsSync blocks the current thread, waiting for that task to complete. Note that the current thread is in that ASP.NET request context, so CarsSync will prevent GetCarsInAWrongWayAsync from ever resuming execution, causing the deadlock.

As a final note, GetCarsInAWrongWayAsync is an OK method. It would be better if it used ConfigureAwait(false), but it's not actually wrong. CarsSync is the method causing the deadlock; it's call to Task<T>.Result is wrong. The appropriate fix is to change CarsSync:

public class HomeController : Controller
{    
  public async Task<ViewResult> CarsSync() 
  {
    SampleAPIClient client = new SampleAPIClient();
    var cars = await client.GetCarsInAWrongWayAsync();
    return View("Index", model: cars);
  }
}
查看更多
登录 后发表回答