Async / wait deadlocking when using SynchronizationContext

According to this link :

When you wait for a method with the await keyword, the compiler generates a bunch of code for you. One of the goals of this action is to handle synchronization with the user interface thread. Key
a component of this function is SynchronizationContext.Current which receives the synchronization context for the current thread.
SynchronizationContext.Current populated depending on where you are. The GetAwaiter method of the task looks for SynchronizationContext.Current . If the current synchronization context is not null, the continuation that is passed to this awaiter will return to this synchronization context.

When using a method that uses the new features of the asynchronous language, in a blocking way you will be stumped if
you have a SynchronizationContext available. . When you consume such methods in a blocking way (waiting in a task using the Wait method or receiving the result directly from the result is the Tasks property), you will block the main thread at the same time. When the task ultimately completes inside this method in the threadpool, it will refer to the continuation to return to the main thread, because SynchronizationContext.Current available and captured. But there is a problem: the user interface thread is blocked and you have a dead end!

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

It’s hard for me to understand the part of the bold above statement, but when I test the above code, it blocks as expected. But I still can not understand why the UI thread is blocked?

In this case, what is available SynchronizationContext ? Is this a user interface thread?

+5
source share
2 answers

I fully explain this in my own blog post , but repeat here ...

await by default capture the current "context" and resume its async method in that context. This context is SynchronizationContext.Current , if it is not null , in which case it is TaskScheduler.Current .

Deadlocks can occur when you have a single-threaded SynchronizationContext time, and you block a task that represents asynchronous code (for example, using Task.Wait or Task<T>.Result ). Note that locking causes a deadlock, not just SynchronizationContext ; the appropriate permission (almost always) is to make the calling code asynchronous (for example, replace Task.Wait / Task<T>.Result with await ). This is especially true for ASP.NET.

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

Your example runs on ASP.NET; no user interface thread.

What is the available SynchronizationContext?

The current SynchronizationContext must be an instance of AspNetSynchronizationContext , a context representing an ASP.NET request. This context allows only one thread at a time.


So, following your example:

When a request arrives for this action, CarsSync will begin execution in this request context. It goes to this line:

 var cars = client.GetCarsInAWrongWayAsync().Result; 

which is essentially the same:

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

So, he goes on to call GetCarsInAWrongWayAsync , which starts until his first await (call GetAsync ) is GetAsync . At this point, GetCarsInAWrongWayAsync captures its current context (ASP.NET request context) and returns an incomplete Task<IEnumerable<Car>> . When the GetAsync download completes, GetCarsInAWrongWayAsync resume executing the ASP.NET request in this context and (eventually) execute the job that it already returned.

However, as soon as GetCarsInAWrongWayAsync returns an incomplete task, CarsSync blocks the current thread, waiting for the completion of this task. Note that the current thread is in the context of an ASP.NET request, so CarsSync prevent CarsSync from GetCarsInAWrongWayAsync , causing a deadlock.

As a final note, GetCarsInAWrongWayAsync is an OK method. It would be better if he used ConfigureAwait(false) , but in fact it is not. CarsSync - a method that causes a dead end; the call to Task<T>.Result . The relevant solution 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); } } 
+8
source

The key point is that some SynchronizationContext allow only one thread to run code at a time. One thread calls Result or Wait . When asynchronous methods want to enter it, it cannot.

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

0
source

Source: https://habr.com/ru/post/1237633/


All Articles