loading...

Why I no longer use ConfigureAwait(false)

noseratio profile image Andrew Nosenko ・3 min read

This might be an unpopular opinion, but I no longer use ConfigureAwait(false) in pretty much any C# code I write, including libraries, unless required by team coding standards.

I mentioned that recently in a comment to "How do you deal with ConfigureAwait(false) everywhere in library code?" on Reddit. I'd like to expand on that a bit in this short blog post, as not using ConfigureAwait(false) in the library code is commonly considered a bad practice.

The two major benefits of using ConfigureAwait(false) and awaiting the returned ConfiguredTaskAwaitable vs the original Task are:

  • Potential performance improvements
  • Potential remedy for deadlocks

Let's get a closer look at each one.

Potential performance improvements

These may be a result of not posting the await continuations to the original synchronization context or task scheduler, but rather continuing in-place, on the same thread where the actual asynchronous operation has completed.

In my opinion, this is much less relevant with the modern ASP.NET Core/ASP.NET 5 back-end, which simply doesn't have a synchronization context by default anymore.

That may be different for the front-end code, which does have a concept of the main thread with certain UI framework-specific synchronization context on it. In this case, I prefer to explicitly control the execution context of the chain of asynchronous APIs I call, especially if it's a part of performance-sensitive code.

I do that by either using Task.Run(Func<Task> asyncFunc):

await Task.Run(async () => 
{
  await RunOneWorkflowAsync();
  await RunAnotherWorkflowAsync();
});

Or, lately, with a custom implementation of TaskScheduler.SwitchTo extension, inspired by this GitHib issue:

await TaskScheduler.Default.SwitchTo();
await RunOneWorkflowAsync();
await RunAnotherWorkflowAsync();

Instead of:

await RunOneWorkflowAsync().ConfigureAwait(false);
await RunAnotherWorkflowAsync();

The former two might be more verbose and could incur an extra thread switch, but they clearly indicate the intent. Moreover, I don't have to worry about any side effects the current synchronization context/task scheduler may have on RunOneWorkflowAsync, and whether or not the author of RunOneWorkflowAsync used ConfigureAwait(false) internally in their implementation.

With the second option, TaskScheduler.Default.SwitchTo is optimized to check if the current thread is already a ThreadPool thread with the default task scheduler, and complete synchronously, if so.

Besides, there are rare corner cases when using ConfigureAwait(false) may actually introduce redundant thread/context switches. Here is some code to illustrate that and my old related question on SO.

Potential remedy for deadlocks

In my option, using ConfigureAwait(false) as a defensive measure against deadlock may actually hide obscure bugs. I prefer detecting deadlocks early. As a last resort, I'd still use Task.Run as a wrapper for deadlock-prone code. Here is a real-life example of where I needed that.

Moreover, from debugging and unit-testing prospective, we always have an option to install a custom SynchronizationContext implementation for debugging asynchronous code, and that would also require to give up ConfigureAwait(false).

Conclusion

Microsoft's Stephen Toub in his excellent "ConfigureAwait FAQ" still recommends using ConfigureAwait(false) for general-purpose, context-agnostic libraries, even if they only target .NET Core or later. Later in the comments to that blog post, David Fowler mentions that "most of ASP.NET Core doesn't use ConfigureAwait(false) and that was an explicit decision because it was deemed unnecessary."

Use your own best judgment on whether you need it or not for a specific project. I've personally chosen to avoid ConfigureAwait(false) where possible, and control the execution context explicitly where it makes sense.

I believe the library code should behave well in any context, and I don't like the idea of using ConfigureAwait(false) as a defensive remedy for undetected deadlocks. Performance-wise, where it is critical (like with ASP.NET Core), there is already no synchronisation context in modern .NET.

As an added bonus, my C# code looks more clean without ConfigureAwait(false) all over the place :-)

Posted on by:

noseratio profile

Andrew Nosenko

@noseratio

Dad, a startup founder, ex-Principal Software Engineer at Nuance Communications, he/him.

Discussion

pic
Editor guide