2012-07-12

Don't Block on Async Code

This is a problem that is brought up repeatedly on the forums and Stack Overflow. I think it's the most-asked question by async newcomers once they've learned the basics.

UI Example

Consider the example below. A button click will initiate a REST call and display the results in a text box (this sample is for Windows Forms, but the same principles apply to any UI application).

// My "library" method.
public static async Task<JObject> GetJsonAsync(Uri uri)
{
  using (var client = new HttpClient())
  {
    var jsonString = await client.GetStringAsync(uri);
    return JObject.Parse(jsonString);
  }
}

// My "top-level" method.
public void Button1_Click(...)
{
  var jsonTask = GetJsonAsync(...);
  textBox1.Text = jsonTask.Result;
}

The "GetJson" helper method takes care of making the actual REST call and parsing it as JSON. The button click handler waits for the helper method to complete and then displays its results.

This code will deadlock.

ASP.NET Example

This example is very similar; we have a library method that performs a REST call, only this time it's used in an ASP.NET context (Web API in this case, but the same principles apply to any ASP.NET application):

// My "library" method.
public static async Task<JObject> GetJsonAsync(Uri uri)
{
  using (var client = new HttpClient())
  {
    var jsonString = await client.GetStringAsync(uri);
    return JObject.Parse(jsonString);
  }
}

// My "top-level" method.
public class MyController : ApiController
{
  public string Get()
  {
    var jsonTask = GetJsonAsync(...);
    return jsonTask.Result.ToString();
  }
}

This code will also deadlock. For the same reason.

What Causes the Deadlock

Here's the situation: remember from my intro post that after you await a Task, when the method continues it will continue in a context.

In the first case, this context is a UI context (which applies to any UI except Console applications). In the second case, this context is an ASP.NET request context.

One other important point: an ASP.NET request context is not tied to a specific thread (like the UI context is), but it does only allow one thread in at a time. This interesting aspect is not officially documented anywhere AFAIK, but it is mentioned in my MSDN article about SynchronizationContext.

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

Preventing the Deadlock

There are two best practices (both covered in my intro post) that avoid this situation:

  1. In your "library" async methods, use ConfigureAwait(false) wherever possible.
  2. Don't block on Tasks; use async all the way down.

Consider the first best practice. The new "library" method looks like this:

public static async Task<JObject> GetJsonAsync(Uri uri)
{
  using (var client = new HttpClient())
  {
    var jsonString = await client.GetStringAsync(uri).ConfigureAwait(false);
    return JObject.Parse(jsonString);
  }
}

This changes the continuation behavior of GetJsonAsync so that it does not resume on the context. Instead, GetJsonAsync will resume on a thread pool thread. This enables GetJsonAsync to complete the Task it returned without having to re-enter the context.

Consider the second best practice. The new "top-level" methods look like this:

public async void Button1_Click(...)
{
  var json = await GetJsonAsync(...);
  textBox1.Text = json;
}

public class MyController : ApiController
{
  public async Task<string> Get()
  {
    var json = await GetJsonAsync(...);
    return json.ToString();
  }
}

This changes the blocking behavior of the top-level methods so that the context is never actually blocked; all "waits" are "asynchronous waits".

Note: It is best to apply both best practices. Either one will prevent the deadlock, but both must be applied to achieve maximum performance and responsiveness.

Resources

This kind of deadlock is always the result of mixing synchronous with asynchronous code. Usually this is because people are just trying out async with one small piece of code and use synchronous code everywhere else. Unfortunately, partially-asynchronous code is much more complex and tricky than just making everything asynchronous.

If you do need to maintain a partially-asynchronous code base, then be sure to check out two more of Stephen Toub's blog posts: Asynchronous Wrappers for Synchronous Methods and Synchronous Wrappers for Asynchronous Methods, as well as my AsyncEx library.

Answered Questions

There are scores of answered questions out there that are all caused by the same deadlock problem. It has shown up on WinRT, WPF, Windows Forms, Windows Phone, MonoDroid, Monogame, and ASP.NET.

16 comments:

  1. Being posted on my birthday this feels like a personal gift.) Thank you for a very helpful post!)

    ReplyDelete
  2. Nice post. Give a better understanding about Asynchronous in C#. Thanks! :)

    ReplyDelete
  3. Being somebody who is currently trying to "formally educate" himself on the subject, I feel that mentioning explicitly the synchronous blocking behavior of Task.Result property would have clarified a lot. I read most of the referred answered questions and most all of the people having the problem were expecting a magic to happen and return the result from the task - a magic similar to what async and await give us, hiding the complexities of CPS programming from us.

    Here's what MSDN has to say about Task.Result property:
    (from here http://msdn.microsoft.com/en-us/library/dd321468.aspx)
    Remarks
    The get accessor for this property ensures that the asynchronous operation is complete before returning.

    ReplyDelete
  4. This saved my day!! Thanks very much

    ReplyDelete
  5. This was very helpful. Every code example wrapped the HttpClient portion with Task.WhenAll or Task.Wait. You explained the deadlock that I suspected. Thank you.

    ReplyDelete
  6. The ConfigureAwait(false) saved my bacon! Tacked it onto the end of GetAsync() and 3 or 4 hours of painful struggle magically disappeared. Cheers! Richard Hammond

    ReplyDelete
  7. Hello,

    First of all, thank you very much for your post. It saved me a very long investigation, I believe.

    I just have on small question. What does it mean, for the asynchronous call, to not collect the synchronization context?

    In my case, I'm in the opposite situation. I got an In-Memory WebAPI server, which I call from a long stack of non-async methods, which in turns is called by MVC (don't ask me why a MVC server would send a request to an In-Memory WebAPI server, I feel we would drift away ;) ). The non-async method waits for the response of the call to the SendRequestAsync of the in-memory server, and in a MVC context, it's the deadlock.

    Therefore, I either have to change all that stack to make it async (and I don't even know if that's possible), or to simply drop the synchronization context capture from the In-Memory WebAPI server, using Configure Await.

    I assume there are some data that would not be available anymore? What's the con, what would I loose?

    Thanks a lot.

    ReplyDelete
    Replies
    1. I assume you mean WCF instead of MVC. It sounds like the root host is still ASP.NET (i.e., your WCF is hosted within ASP.NET).

      You can look at your current context by doing a "Debug.WriteLine(SynchronizationContext.Current.GetType().Name)".

      To solve the problem, it's best to actually make everything async (WCF in 4.5 does have built-in support for asynchronous implementations, and if you make your server async it won't affect your clients at all). If you can't do that, then you can put in a workaround such as ConfigureAwait. Stephen Toub describes several options here: http://blogs.msdn.com/b/pfxteam/archive/2012/04/13/10293638.aspx

      The disadvantage to ConfigureAwait is that you lose your request context after the first await. So you can't access anything on the HttpContext, or depend on things like culture being set appropriately.

      Delete
  8. I'm using .net 4.0 is there a ConfigureAwait equivalent?

    ReplyDelete
    Replies
    1. If you have Microsoft.Bcl.Async installed, you should have ConfigureAwait available.

      Delete
  9. Is there any way to detect a deadlock? I just tried your WinForms example and see exactly what you're talking about - looks like it will hang forever without ever returning. I doubt there's anything we could do at compile-time, but at runtime, is there a global "deadlock detection" concept or anything that would help troubleshoot something like this? Like in SQL Server where it will eventually kill off one of the connections that's involved in a deadlock.

    Of course if we build things properly, deadlocks won't happen, but I can see this potentially being a common mistake.

    ReplyDelete
    Replies
    1. The problem with deadlock detection is that you have to hook the Wait or Result calls; this should be possible with the profiling API. Alternatively, Joe Duffy has a blog post on deadlock detection but you need to provide your own SynchronizationContext (in this case, it would need to be a wrapper for the WinFormsSynchronizationContext): http://joeduffyblog.com/2008/02/27/hooking-clr-blocking-calls-with-synchronizationcontext/

      Either way, it would be quite a lot of effort. I think it's not worth it; in most cases, if a deadlock happens, it *always* happens. So we're not dealing with a deadlock caused by a race condition; this is usually just a one-time learning experience, not a dormant bug kind of problem.

      Delete
  10. After reading this the clouds lifted. Thanks a TON for this post.

    ReplyDelete
  11. Hey Stephen,
    Thanks a lot for all the article you've published thus far. They are super useful.

    I have a question for you. In your code solution 2 above

    public class MyController : ApiController
    {
    public async Task Get()
    {
    var json = await GetJsonAsync(...);
    return json.ToString();
    }
    }

    Now if you make the Get() method async and you call GetJsonAsync() and if this method is a long running process, wouldn't the request just return back to the client without actually waiting for the GetJsonAsync() call to complete. Could you please shed some light on this.

    Thanks.

    ReplyDelete
    Replies
    1. No, it will not return to the client. The ASP.NET MVC runtime sees that the action is returning a Task, and it will (asynchronously) wait for that task to complete before sending the response to the client.

      Delete