Async-Await Best Practices and Caveats in C#

Posted in category csharp on 2014-06-29

Table of Contents

Based on the TechEd talk

Reasoning about order of things

The following example demonstrates basic method execution principle when using async-await.

string _response;

async void ClickMeButton_Click(object sender, EventArgs e) {
  try {
    LoadDataAsync("http://www.google.com");
    await Task.Delay(2000);
    Logger.Info("Received Data: " + _response);
  }
  catch (Exception ex) {
    Logger.Error("Error posting data to server." + ex.Message);
  }
}

async void LoadDataAsync(string url) {
  var request = WebRequest.Create(url);
  using (var response = await request.GetResponseAsync()) {
    var stream = response.GetResponseStream();
    using (var reader = new StreamReader(stream)) {
      _response = reader.ReadToEnd();
    }
  }
}

Once the code execution reaches await, methods return (assuming that the rest of the method execution will be resumed later). So, question is which await of these two will be resumed first? What if one of the times network connection is slow? Basically, it’s not deterministic with all its consequences.

Strange exceptions

This is a slightly modified example - with only one await in the LoadData method body.

string _response;

async void ClickMeButton_Click(object sender, EventArgs e) {
  try {
    LoadDataAsync("http://www.google.com");
  }
  catch (Exception ex) {
    Logger.Error("Error posting data to server." + ex.Message);
  }
}

async void LoadDataAsync(string url) {
  var request = WebRequest.Create(url);
  using (var response = await request.GetResponseAsync()) {
    var stream = response.GetResponseStream();
    using (var reader = new StreamReader(stream)) {
      _response = reader.ReadToEnd();
    }
  }
}

Since in the button click handler method there is no await method expecting the result from LoadData, its execution is very fast (moreover, it’s not strictly necessary to use async in the signature of this method since there is no await construct in its body). And thus, when execution of LoadData completes with exception there is no calling host context any longer (ClickMeButton_Click method is long done). And so, it will cause an undesirable effect of crashing the main UI thread of your application, for example.

It makes sense to stress that since LoadData returns void, it’s impossible to await a call to it. In another words, async void methods are simple fire-and-forget methods.

It’s a good practice though to make sure async methods return Task, such that it is always possible to await it and properly deal with the result - either return value or thrown exception. There are very few situations when async void might be excused - top-level event handlers or other situations when fire-and-forget scenario is also applicable. But even in such cases it might make sense to return async Task instead of async void and let caller decide if it can be used or ignored.

Now, let’s see how easy that is to fix it:

string _response;

async void ClickMeButton_Click(object sender, EventArgs e) {
  try {
    await LoadDataAsync("http://www.google.com");
  }
  catch (Exception ex) {
    Logger.Error("Error posting data to server." + ex.Message);
  }
}

async Task LoadDataAsync(string url) {
  var request = WebRequest.Create(url);
  using (var response = await request.GetResponseAsync()) {
    var stream = response.GetResponseStream();
    using (var reader = new StreamReader(stream)) {
      _response = reader.ReadToEnd();
    }
  }
}

Parallelizing IO-bound tasks

Parallelizing CPU-bound tasks is rather trivial thing. Just rely on the thread pool and use one of your favorite features in C# - Parallel.For, AsParallel, etc.

Parallelizing IO-bound tasks is a different thing. Here we have a little bit of waiting involved in each task execution. Let’s look into the example where we need to load information about houses from the database. Loading information about single house is a task that requires a lot of waiting - sending the request to the database and fetching the result back are the most time consuming operations in this process. Here is how it’s done in a sequential manner where we load one entry after another:

List<House> SequentialLoadHouses(int first, int last) {
  var houses = new List<House>();
  for (var x = first; x <= last; x++) {
    var house = House.LoadFromDatabase(x);
    houses.Add(house);
  }
  return houses;
}

And the reason for that is that each thread on the thread pool is spending its CPU time waiting for IO to complete.

On the other hand, here is an optimal version which fires off all the IO-bound tasks and awaits the completion of all of them in order to proceed.

async Task<List<House>> OptimalLoadHousesAsync(int first, int last) {
  var tasks = new List<Task<House>>();
  for (var x = first; x <= last; x++) {
    var task = House.LoadFromDatabaseAsync(x);
    tasks.Add(task);
  }
  var loadedHouses = await Task.WhenAll(tasks);
  return loadedHouses.ToList();
}

This version has only one drawback - it relies on the thread-pool that will use as many threads concurrently as many CPUs are there available on your computer. In most cases this would make sense, but would that make sense on a powerful server with lots and lots of CPUs available - will that make sense or be fare to use all available CPUs by just one application?

Trivial test with fixed time spent waiting for dummy IO-bound task to complete (100 milliseconds) shows the following numbers (taken that the first = 10 and last = 1000):

SequentialLoadHouses:    00:01:39.6802
NotSoOptimalLoadHouses:  00:00:10.0551
OptimalLoadHousesAsync:  00:00:06.8445

Here is the test code itself:

const int first = 10;
const int last = 1000;

// sequential
var swSeq = Stopwatch.StartNew();
HouseLoader.SequentialLoadHouses(first, last);
var seqTiming = swSeq.Elapsed; // 00:01:39.6802
swSeq.Stop();

// suboptimal
var swNotOpt = Stopwatch.StartNew();
HouseLoader.SuboptimalLoadHouses(first, last);
var notOptTiming = swNotOpt.Elapsed; // 00:00:10.0551
swNotOpt.Stop();

// optimal
var swOpt = Stopwatch.StartNew();
await HouseLoader.OptimalLoadHousesAsync(first, last);
var optTiming = swOpt.Elapsed; // 00:00:06.8445
swOpt.Stop();

Async over Events

Async-await are there for composing async operations. But in case when a task needs to be created, TaskCompletionSource is to be used:

async Task PlaySound(Storyboard storyboard) {
  var tcs = new TaskCompletionSource<object>();
  EventHandler lambda = (s, e) => tcs.TrySetResult(null);
  try {
    storyboard.Completed += lambda;
    storyboard.Begin();
    await tcs.Task;
  }
  finally {
    storyboard.Completed -= lambda;
  }
}

This way we turn events into async methods.

Best Practices

It is considered to be a good practice for synchronous methods to run CPU-bound tasks, such that the user of such method would be able to guess that while this synchronous method is running it spins your CPUs.

It is considered to be a good practice for asynchronous methods to run IO-bound tasks primarily, such that user of such method would be able to guess that while this asynchronous method is running it does not spin you CPUs heavily.

Here is an example of a synchronous method that prints message to the console after 10 seconds delay:

void PausePrint(string message) {
  var end = DateTime.Now + TimeSpan.FromSeconds(10);
  while (DateTime.Now < end) { }
  Console.WriteLine(message);
}

This method is spinning your CPUs while waiting for 10 seconds to pass, and then prints out supplied message to the console.

The following is the asynchronous implementation of the same method:

async Task PausePrintAsync(string message) {
  await Task.Delay(10000);
  Console.WriteLine(message);
} 

These two methods are fare according to the signature they have and considered to be a good examples for a library that exposes these signatures publicly.

Though, it is considered to be a bad practice when an asynchronous method relies on a synchronous implementation, or the other way round (for a library that exposes such signatures publicly):

Task BadPausePrintAsync(string message) {
  return Task.Run(() => PausePrint(message));
} 

Generally, using Task.Run in a library is considered to be a very bad thing. Tweaking threads and other resources are good in the applications, not libraries. So, in order to leave application developers a chance to reason about thread pool resources, it’s best to avoid using Task.Run in your libraries.

void BadPausePrint(string message) {
  var task = PausePrintAsync(message);
  task.Wait();
}

This one, where developer wraps an asynchronous call in a synchronous method, is the most dangerous one of all. Assume, that BadPausePrint method is running on the UI thread. It fires an asynchronous method and waits for the result. When asynchronous method completes, it publishes it’s result to the UI thread, which is currently blocked, causing the dead-lock.

It’s not that terrible when BadPausePrint method is running on the thread-pool thread, since thread-pool is generally smart figuring that it should resume asynchronous method on a different thread it was started.

Source