Tips for Using Task.Run With Async/Await

Where to Put a Call to Task.Run

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();
}
});
}

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:

static async Task ProcessImage(byte[] imageData)
{
await Task.Run(() =>
{
RotateImage(imageData);
DarkenImage(imageData);
BlurImage(imageData);
}
}

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:

await Task.Run(async () => 
{
RotateImage(imageData);
DarkenImage(imageData);
await BlurImage(imageData);
}

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:

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);
}

…and BlurImage would simply be:

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();
}
}

Don’t Continue on the Main Thread Unnecessarily

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:

static async void OnButtonClick()
{
byte[] imageData = await LoadImage();
Task processImageTask = Task.Run(() => ProcessImage(ref imageData));
await processImageTask.ConfigureAwait(false);
await SaveImage(imageData);
}

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:

static async void OnButtonClick()
{
byte[] imageData = await LoadImage();
await Task.Run(() => ProcessImage(ref imageData)).ConfigureAwait(false);
await SaveImage(imageData);
}

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.

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?

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?

Conclusion

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store