DEV Community

Andrew Nosenko
Andrew Nosenko

Posted on • Updated on

Why I no longer use ConfigureAwait(false)

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

The following two things are widely considered as major benefits of calling task.ConfigureAwait(false) and awaiting the returned ConfiguredTaskAwaitable value, rather than the original task:

  • Potential performance improvements
  • Potential remedy for deadlocks

Let's break it down.

Potential performance improvements

It might be the result of not queueing the await continuation callbacks with the original synchronization context/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+ backend, which simply doesn't have a synchronization context by default anymore. ConfigureAwait(false) simply becomes a no-op in that execution environment.

On the other hand, the front-end code normally does have a concept of the main UI thread, with a certain UI framework-specific synchronization context installed on it (e.g, DispatcherSynchronizationContext).

In this case, I believe that using ConfigureAwait(false) for the purpose of not continuing on a UI thread is a premature optimization, especially for small code chunks between awaits. It may actually have a negative effect, by introducing frequent yet redundant context switches. In a nutshell (contrived but illustrative):

async Task SomeAsync(Func<Task> func)
{
    var threadId = Environment.CurrentManagedThreadId;
    await func().ConfigureAwait(false);
    // false: a redundant and probably undesired thread switch here
    Debug.Assert(threadId == Environment.CurrentManagedThreadId);
}

private async void MainForm_Load(object? sender, EventArgs e)
{
    var tcs = new TaskCompletionSource();
    var task = SomeAsync(() => tcs.Task);
    tcs.SetResult();
    await task;
}
Enter fullscreen mode Exit fullscreen mode

More details in my old related question on SO.

Thus, for the UI threads, I prefer to explicitly control the execution context, especially where it's a part of performance-sensitive, CPU-intensive code.

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

await Task.Run(async () => 
{
  await RunOneWorkflowAsync();
  await RunAnotherWorkflowAsync();
});
Enter fullscreen mode Exit fullscreen mode

Or, lately, with a custom implementation of TaskScheduler.SwitchTo extension, inspired by this David Fowler's initiative:

await TaskScheduler.Default.SwitchTo();
await RunOneWorkflowAsync();
await RunAnotherWorkflowAsync();
Enter fullscreen mode Exit fullscreen mode

Instead of:

await RunOneWorkflowAsync().ConfigureAwait(false);
await RunAnotherWorkflowAsync();
Enter fullscreen mode Exit fullscreen mode

The former two might be a bit 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.

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 :-)

Discussion (7)

Collapse
eldenis profile image
Denis J. González

Thanks for this article! I had been thinking about this for a while since I first saw someone using ConfigureAwait(false) in an ASP.NET Core 2.1 project and it didn't make sense to me.

Collapse
erdemyavuzyildiz profile image
ERDEM YAVUZ YILDIZ

Correct, It's not library's responsibility to take defense against certain situations while also limiting it's own usability.
Developers should be aware of any synchronization context in use and code accordingly.
Library developer's decisions can't remedy for that. Trying to solve developer's problem with a library is bad practice alright.

Developer's should be also free to develop their own synchronization context and call library functions within that context.

Collapse
noseratio profile image
Andrew Nosenko Author

I agree, but many would disagree.

Collapse
backwardsdave1 profile image
David Klempfner

Very good article, thanks for writing.
Btw you spelled Sydney wrong in your profile.

Collapse
noseratio profile image
Andrew Nosenko Author

Thank you, fixed :)

Collapse
paulomorgado profile image
Paulo Morgado

ConfigureAwait(false) is about not returning back to the synchronization context when an awaiter completes.

It's about performance, not deadlocks.

Collapse
noseratio profile image
Andrew Nosenko Author

I believe I've covered that in the article in this section:

• Potential performance improvements