DEV Community

G.L Solaria
G.L Solaria

Posted on

C# Async/Await Mistakes: Resuming in the Wrong Context

Using async/await in C# is generally straight forward but when you start to use it in more advanced ways, you need to understand a bit more about what is happening to make sure you are not making mistakes.

Let's look at one common mistake: resuming in the wrong context.

The code below implements a class that uses HTTP Client to load some JSON. The LoadToDosAsync method however has a potential problem. Can you spot it?

public static class Getter
{
  private static HttpClient _Client = new()
  {
    BaseAddress = 
      new Uri("https://jsonplaceholder.typicode.com/"),
  };

  public static async Task LoadToDosAsync()
  {
      using var response = await _Client.GetAsync("todos/1");

      response.EnsureSuccessStatusCode();

      var json = await response.Content.ReadAsStringAsync();

      // This is a CPU intensive operation...
      LoadData(json);
  }
}
Enter fullscreen mode Exit fullscreen mode

When LoadToDosAsync is called, the calling thread returns when it hits the first await. A thread from the threadpool will then be selected by the threadpool scheduler and this thread will perform the work to call out over the network to get the data.

If LoadToDosAsync is called from a main UI thread and when GetAsync returns, execution will resume on the main UI thread until it reaches the next await.

If LoadToDosAsync is called from the main of a console application and when GetAsync returns, execution will resume in the context of the threadpool scheduler. The threadpool scheduler will then select a thread from the threadpool that will execute until it hits the next await.

A similar process is then repeated when awaiting ReadAsStringAsync.

So LoadData ends up being executed in the same context as LoadToDosAsync started executing before the first await. The context will be the main UI thread if LoadToDosAsync is called from the main UI thread. The context will be the threadpool scheduler if called from the main of a console application.

What's the potential problem here? The problem shows up when LoadToDosAsync is called from a main UI thread. Execution resumes on the main UI thread when ReadAsStringAsync returns hence LoadData will execute on the main UI thread. LoadData is a CPU intensive call that will block the main UI thread until it completes. This means the UI will freeze until LoadData completes. So the UI will be unresponsive while LoadData is executing.

To fix this problem, you can use ConfigureAwait(false) to indicate that execution can resume in the threadpool scheduler context. The threadpool scheduler will then select a thread from the threadpool to continue execution of LoadData instead of executing it in the main UI thread.

public static class Getter
{
    private static HttpClient _Client = new()
    {
        BaseAddress = 
            new Uri("https://jsonplaceholder.typicode.com/"),
    };

    public static async Task GetToDosAsync()
    {
        using var response = await _Client
          .GetAsync("todos/1")
          .ConfigureAwait(false);

        response.EnsureSuccessStatusCode();

        var json = await response.Content
          .ReadAsStringAsync()
          .ConfigureAwait(false);

        // This is a CPU intensive operation...
        LoadData(json);
    }
}
Enter fullscreen mode Exit fullscreen mode

Using ConfigureAwait(false) is especially important when writing general-purpose libraries. You should get in the habit of using it almost everywhere unless you know you want to resume in the caller's context.

It must be noted that if the calling context is the threadpool scheduler, you may or may not resume on the same calling thread. Take the example of a console application below...

while (true)
{
    int callingThread = Environment.CurrentManagedThreadId;
    await Task.Delay(TimeSpan.FromSeconds(1));
    int resumingThread = Environment.CurrentManagedThreadId;

    Console.WriteLine(
      $"Calling {callingThread}, Resumed {resumingThread}.");
}
Enter fullscreen mode Exit fullscreen mode

Run this console application and you will see that sometimes execution resumes on the calling thread but sometimes it does not.

Calling 1, Resumed 5.
Calling 5, Resumed 5.
Calling 5, Resumed 5.
Calling 5, Resumed 7.
Calling 7, Resumed 7.
Enter fullscreen mode Exit fullscreen mode

It should be noted that the above is a simple explanation of when is going on. To read a technically correct explanation of the behaviour of ConfigureAwait(false) refer to Stephen Toub's article on the subject.

Top comments (2)

Collapse
 
sduduzog profile image
Sdu

Wouldn't it be better to wrap LoadData in a await Task.run(()... call instead? I feel like .ConfigureAwait(false) is a lot noisier than the latter.

What are the tradeoffs?

Collapse
 
glsolaria profile image
G.L Solaria • Edited

I think in this case it depends on what's the expected usage of the class. Say this example was instead ...

public async Task<ReadOnlyCollection<ToDo>> GetToDosAsync()
{
     using var response = await _Client
       .GetAsync("todos/1")
       .ConfigureAwait(false);

     response.EnsureSuccessStatusCode();

     var json = await response.Content
       .ReadAsStringAsync()
       .ConfigureAwait(false);

     // This is a CPU intensive operation that loads the data 
     // into the private data structure _list.
     LoadData(json);

     return _list.AsReadOnly();
}
Enter fullscreen mode Exit fullscreen mode

In this case LoadData cannot be a fire and forget operation because it loads data into a data structure that gets returned.

ConfigureAwait(false) is annoying to write everywhere in general-purpose libraries and solutions have been openly debated as far back as 2015 in dotnet/csharp github issues but as far as I am aware Stephen Toub doesn't think it can be changed (e.g. Make ConfigureAwait(false) the default behaviour for Tasks).