An async/await example that causes a deadlock

Take a look at this example, Stephen has a clear answer for you:

So this is what happens, starting with the top-level method (Button1_Click for UI / MyController.Get for ASP.NET):

  1. The top-level method calls GetJsonAsync (within the UI/ASP.NET context).

  2. GetJsonAsync starts the REST request by calling HttpClient.GetStringAsync (still within the context).

  3. GetStringAsync returns an uncompleted Task, indicating the REST request is not complete.

  4. GetJsonAsync awaits the Task returned by GetStringAsync. The context is captured and will be used to continue running the GetJsonAsync method later. GetJsonAsync returns an uncompleted Task, indicating that the GetJsonAsync method is not complete.

  5. The top-level method synchronously blocks on the Task returned by GetJsonAsync. This blocks the context thread.

  6. … Eventually, the REST request will complete. This completes the Task that was returned by GetStringAsync.

  7. The continuation for GetJsonAsync is now ready to run, and it waits for the context to be available so it can execute in the context.

  8. Deadlock. The top-level method is blocking the context thread, waiting for GetJsonAsync to complete, and GetJsonAsync is waiting for the context to be free so it can complete. For the UI example, the “context” is the UI context; for the ASP.NET example, the “context” is the ASP.NET request context. This type of deadlock can be caused for either “context”.

Another link you should read: Await, and UI, and deadlocks! Oh my!

  • Fact 1: GetDataAsync().Result; will run when the task returned by GetDataAsync() completes, in the meantime it blocks the UI thread
  • Fact 2: The continuation of the await (return result.ToString()) is queued to the UI thread for execution
  • Fact 3: The task returned by GetDataAsync() will complete when its queued continuation is run
  • Fact 4: The queued continuation is never run, because the UI thread is blocked (Fact 1)

Deadlock!

The deadlock can be broken by provided alternatives to avoid Fact 1 or Fact 2.

  • Avoid 1,4. Instead of blocking the UI thread, use var data = await GetDataAsync(), which allows the UI thread to keep running
  • Avoid 2,3. Queue the continuation of the await to a different thread that is not blocked, e.g. use var data = Task.Run(GetDataAsync).Result, which will post the continuation to the sync context of a threadpool thread. This allows the task returned by GetDataAsync() to complete.

This is explained really well in an article by Stephen Toub, about half way down where he uses the example of DelayAsync().

I was just fiddling with this issue again in an ASP.NET MVC project. When you want to call async methods from a PartialView, you’re not allowed to make the PartialView async. You’ll get an exception if you do.

You can use the following simple workaround in the scenario where you want to call an async method from a sync method:

  1. Before the call, clear the SynchronizationContext
  2. Do the call, there will be no more deadlock here, wait for it to finish
  3. Restore the SynchronizationContext

Example:

public ActionResult DisplayUserInfo(string userName)
{
    // trick to prevent deadlocks of calling async method 
    // and waiting for on a sync UI thread.
    var syncContext = SynchronizationContext.Current;
    SynchronizationContext.SetSynchronizationContext(null);

    //  this is the async call, wait for the result (!)
    var model = _asyncService.GetUserInfo(Username).Result;

    // restore the context
    SynchronizationContext.SetSynchronizationContext(syncContext);

    return PartialView("_UserInfo", model);
}

Leave a Comment