Why I no longer use ConfigureAwait(false)
Tags: dotnet, csharp, async
Published: 2020-09-17 00:37:25

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 the team coding standards.

The following two points are widely seen as major benefits of calling `task.ConfigureAwait(false)` and awaiting the [`ConfiguredTaskAwaitable`](https://docs.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.configuredtaskawaitable) value it returns, 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. Under particular circumstances, it may even have a negative effect, by introducing frequent yet redundant context switches. In a nutshell (contrived but illustrative):

```c#
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;
}
```

More details in my [old related question on SO](https://stackoverflow.com/q/28410046/1768303).

Thus, for the UI threads, I prefer to explicitly control the execution context, especially where it is a part of performance-sensitive, CPU-intensive code.
 
I can do that by either using [`Task.Run(Func<Task> asyncFunc)`](https://docs.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.run?view=netcore-3.1#System_Threading_Tasks_Task_Run_System_Func_System_Threading_Tasks_Task__):

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

Or, lately, with [a custom implementation](https://gist.github.com/noseratio/5d2d5f2a0cbb71b7880ce731c3958e62) of `TaskScheduler.SwitchTo` extension, inspired by [this David Fowler's initiative](https://github.com/dotnet/runtime/issues/20025):

```C#
await TaskScheduler.Default.SwitchTo();
await RunOneWorkflowAsync();
await RunAnotherWorkflowAsync();
```

Instead of:

```C#
await RunOneWorkflowAsync().ConfigureAwait(false);
await RunAnotherWorkflowAsync();
```

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](https://gist.github.com/noseratio/5d2d5f2a0cbb71b7880ce731c3958e62#file-taskschedulerawaiter-cs-L53) 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](https://stackoverflow.com/a/28445791/1768303) 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"](https://devblogs.microsoft.com/dotnet/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](https://devblogs.microsoft.com/dotnet/configureawait-faq/#comment-4247) 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 :-)