danielwertheim

danielwertheim


notes from a passionate developer

Share


Sections


Tags


Disclaimer

This is a personal blog. The opinions expressed here represent my own and not those of my employer, nor current or previous. All content is published "as is", without warranty of any kind and I don't take any responsibility and can't be liable for any claims, damages or other liabilities that might be caused by the content.

Do I need to use ConfigureAwait(false) all the way?

Using ConfigureAwait(false) is said to be a recommendation for e.g. third party library authors. Why? Well, lets say you are building a ASP.Net API hosted via IIS and consume a library that offers an async API for e.g. I/O bound work. When using it you "manage" to introduce a blocking call via e.g. Task.Result higher up the call stack and suddenly you start experiencing dead-locks. This happens when the async task is completed and the captured context (where the task was created within) is being "restored" on a different thread (more info). By using ConfigureAwait(false) you explicitly state "do not capture any context" and thereby you avoid the problem. Right. But do you have to do it all the way? Lets see.

Before looking at the sample lets clear something out. When consuming asynchronous code, you should of course strive to be asynchronous all the way and don't introduce any blocking calls. If you need to, you can push it up as far as possible and wrap the code within, a by you started Task, e.g. via Task.Run.

Remote Web API

There's an API hosted at http://localhost:53573/customers/{id} that represents an API "out of our control". All it does is to return some fake Customer info e.g.

{
  "Id": "759",
  "Name": "Customer 759",
  "Score": 1968
}

Library

The Remote API is accessed via a service in a library (NuGet or whatever) that doesn't make use of ConfigureAwait(false):

public class CustomerScoreService
{
  public async Task<Customer> GetCustomerAsync(string id)
  {
    using (var client = new HttpClient
    {
      DefaultRequestHeaders = {
        Accept = {
          new MediaTypeWithQualityHeaderValue("application/json")
        }
      }
    })
    {
      var json = await client
        .GetStringAsync($"http://localhost:53573/customers/{id}");

      return Deserialize<Customer>(json);
    }
  }

  private static T Deserialize<T>(string json)
  {
    if (string.IsNullOrWhiteSpace(json))
      return default(T);

    return JsonConvert.DeserializeObject<T>(json);
  }
}

Our Web API

The CustomerScoreService is used from a second ASP.Net WebAPI controller, one that is under our control.

[RoutePrefix("test")]
public class CustomersController : ApiController
{
  private readonly CustomerScoreService _service;

  public CustomersController()
  {
    _service = new CustomerScoreService();
  }
  [Route("blocking/{id}")]
  [HttpGet]
  public Customer GetViaAsyncService(string id)
  {
    return SomeInternalThingOneAsync(id).Result;
  }

  private async Task<Customer> SomeInternalThingOneAsync(string id)
  {
    var customer = await SomeInternalThingTwoAsync(id).ConfigureAwait(false);

    //Do some stuff otherwise this could have elided instead of awaiting

    return customer;
  }

  private async Task<Customer> SomeInternalThingTwoAsync(string id)
  {
    var customer = await _service.GetCustomerAsync(id);

    //Do some stuff otherwise this could have elided instead of awaiting

    return customer;
  }
}

Doing a request in Postman against the API with to controller above:

GET http://localhost:53616/test/blocking/{{$randomInt}}

yields a lock issue.

Adding ConfigureAwait(false) to the "second level":

private async Task<Customer> SomeInternalThingTwoAsync(string id)
{
  var customer = await _service.GetCustomerAsync(id).ConfigureAwait(false);

  return customer;
}

Trying the request in Postman again, still yields a lock issue.

Adding ConfigureAwait(false) to the CustomerScoreService:

public async Task<Customer> GetCustomerAsync(string id)
{
  using (var client = new HttpClient
  {
    DefaultRequestHeaders = {
      Accept = {
        new MediaTypeWithQualityHeaderValue("application/json")
      }
    }
  })
  {
    var json = await client
      .GetStringAsync($"http://localhost:53573/api/customers/{id}")
      .ConfigureAwait(false);

    return Deserialize<Customer>(json);
  }
}

Yet again trying the request in Postman:

{
  "Id": "243",
  "Name": "Customer 243",
  "Score": 4516
}

Success! No dead-lock issues now that we have ensured ConfigureAwait(false) usage in all "levels".

Summary

As the sample showed, we do get different behaviour when using ConfigureAwait(false) in all levels. Again. The best would be to always be asynchronous all the way, in other words: do not introduce any blocking calls.

Please do note that the behaviour will vary depending on where it's called from. An application running in a console application will not be affected by this as one running in ASP.Net hosted in IIS.

Feel free to provide corrections, thoughts, feedback etc.

Cheers,

//Daniel

View Comments