Author avatar

Nate Cook

Advanced Tips for Using Task.Run With Async/Await

Nate Cook

  • Feb 22, 2019
  • 8 Min read
  • 1,565 Views
  • Feb 22, 2019
  • 8 Min read
  • 1,565 Views
Languages Frameworks and Tools
C#

Deepening Our Understanding

In the previous guide in this series we saw why Task.Run is mainly useful for CPU-bound code. In exploring that topic it became clear that, although you can use Task.Run with other types of operations, it may not be the best use of system resources. We also saw how easy it is to await a call to Task.Run. But that's certainly not all there is to it. There are a few tips that can save you a lot of headaches if you know them.

Where to Put a Call to Task.Run

Let's pick up where we left off with the application from the previous guide. That application downloaded an image and then blurred it using a library called ImageSharp (available in NuGet as SixLabors.ImageSharp). We defined a method called BlurImage as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
static async Task<byte[]> BlurImage(string imagePath)
{
  return await Task.Run(() =>
  {
    var image = Image.Load(imagePath);
    image.Mutate(ctx => ctx.GaussianBlur());
    using (var memoryStream = new MemoryStream())
    {
      image.SaveAsJpeg(memoryStream);
      return memoryStream.ToArray();
    }
  });
}
csharp

Notice that the call to Task.Run is immediately before the image processing code. Is that the best approach?

Whether for convenience or clarity, you might find yourself putting a call to Task.Run as close as possible to the CPU intensive code, much like in the above method. As your application increases in complexity though, this turns out to be suboptimal. To illustrate this, imagine if in the future we wanted to add a method to our application that would rotate, darken, and blur. We might start by writing something like the following:

1
2
3
4
5
6
7
8
9
static async Task ProcessImage(byte[] imageData)
{
  await Task.Run(() => 
  {
    RotateImage(imageData);
    DarkenImage(imageData);
    BlurImage(imageData);
  }
}
csharp

But then we notice that BlurImage (or a version of it that accepts a byte array) already returns a Task, so we change it to:

1
2
3
4
5
6
await Task.Run(async () => 
{
  RotateImage(imageData);
  DarkenImage(imageData);
  await BlurImage(imageData);
}
csharp

And then we notice that BlurImage itself calls Task.Run, which means we now have a nested Task.Run call. So we would be launching a thread from within another thread. This is again, not the best use of system resources, and will probably have a negative impact on performance. This is why library authors are discouraged from using Task.Run in library methods: It should be up to the caller when threads are launched.

Therefore, it's generally recommended that you put calls to Task.Run as close to the UI code and event handlers as possible. In following that recommendation you'll find that most CPU-bound code ends up being written as synchronous, and Task.Run goes in the outermost calling method. So, in this example, we'd end up with something like:

1
2
3
4
5
6
7
8
9
10
11
12
13
static async void OnButtonClick()
{
  byte[] imageData = await LoadImage();
  await Task.Run(() => ProcessImage(ref imageData));
  await SaveImage(imageData);
}

static void ProcessImage(ref byte[] imageData)
{
  RotateImage(ref imageData);
  DarkenImage(ref imageData);
  BlurImage(ref imageData);
}
csharp

...and BlurImage would simply be:

1
2
3
4
5
6
7
8
9
10
static void BlurImage(ref byte[] imageData)
{
  var image = Image.Load(imageData);
  image.Mutate(ctx => ctx.GaussianBlur());
  using (var memoryStream = new MemoryStream())
  {
    image.SaveAsJpeg(memoryStream);
    imageData = memoryStream.ToArray();
  }
}
csharp

Don't Continue on the Main Thread Unnecessarily

As you probably recall, await captures information about the current thread when used with Task.Run. It does that so execution can continue on the original thread when it is done processing on the other thread. But what if the rest of the code in the calling method doesn't need to be run on the original thread?

In that case, it turns out you can give your code a bit of a performance boost by telling await that you don't want to continue in the original context. This is done by using a Task method called ConfigureAwait. A good example would be in the OnButtonClick method we defined earlier:

1
2
3
4
5
6
7
static async void OnButtonClick()
{
  byte[] imageData = await LoadImage();
  Task processImageTask = Task.Run(() => ProcessImage(ref imageData));
  await processImageTask.ConfigureAwait(false);
  await SaveImage(imageData);
}
csharp

Defining a variable just for that is a bit verbose though, so most of the time one would just attach it to the end of the call to Task.Run:

1
2
3
4
5
6
static async void OnButtonClick()
{
  byte[] imageData = await LoadImage();
  await Task.Run(() => ProcessImage(ref imageData)).ConfigureAwait(false);
  await SaveImage(imageData);
}
csharp

The parameter to ConfigureAwait is a boolean named continueOnCapturedContext, and the default is true. By passing false instead, we're indicating that we wish to continue the rest of the method on a thread pool thread instead of the UI thread. As long as you're not modifying any UI elements in the code following the await (or doing anything else there that would require the main thread of your application), you can safely use this technique to enable an amount of parallelism.

Library authors that use await are especially encouraged to use ConfigureAwait(false), as not doing so can cause deadlocks depending on how application developers are consuming your library methods. Most of the time library code does not care what thread it runs on, so using ConfigureAwait(false) will ensure you library code is never waiting on the main thread.

Now I must admit, ConfigureAwait(false) is not the greatest syntax, and its presence does clutter the code somewhat. Indeed, I wish there were a better way. But the less you run on your application's main thread, the faster the application will seem to the end user. So do use ConfigureAwait(false) where applicable. Your application's users will thank you!

Should I Use Task.Run With ASP.NET Core?

Thus far we've talked about UI-based applications, but does this information about Task.Run apply to a web application framework such as ASP.NET Core?

There are certainly a number of advantages to using async/await with ASP.NET Core, but the same cannot be said for Task.Run. As it turns out, using thread pool threads doesn't make much sense when you're serving a web page. Generally, with ASP.NET there is one thread per request and you want to be able to handle as many requests concurrently as possible. Using Task.Run in that context actually reduces scalability because you're reducing the number of threads available to handle new requests. Furthermore, using a separate thread won't do anything for responsiveness, since the user is not able to interact with the page until it is loaded anyway. And once the page is loaded, responsiveness is primarily determined by the user's client-side browser interactions (and the quality of the JavaScript code), not by ASP.NET. So, for CPU-bound code in ASP.NET, it's best to stick to synchronous processing. In short, avoid using Task.Run in ASP.NET applications. If you are using async/await, focus on the naturally asynchronous I/O operations!

What About Task.Factory.StartNew?

You may stumble across a similar method at Task.Factory.StartNew and be curious about it, so it is worth a brief mention. Actually, the method at Task.Factory.StartNew was introduced before Task.Run and is more configurable. However, it's best to stick with Task.Run. Except for a few very specific needs that are well outside normal application requirements (not to mention the scope of this guide), you really never need the additional complexity that Task.Factory.StartNew provides, and Task.Run is more succinct anyway. Don't try to get clever; just use Task.Run!

Conclusion

Understanding when and how to use Task.Run is important for any C# developer wanting to keep their applications responsive. As we saw with ASP.NET, sometimes the answer is to not use Task.Run at all! By contrast, for applications with user interfaces, it's the primary way to run CPU-bound code in a non-blocking fashion. For those situations, keep in mind the best practice regarding where to put the calls to Task.Run, and you'll surely find success using it conjunction with async/await.

9