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
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):
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.
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)
:
await Task.Run(async () =>
{
await RunOneWorkflowAsync();
await RunAnotherWorkflowAsync();
});
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();
Instead of:
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 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 :-)
Top comments (7)
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.
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.
I agree, but many would disagree.
Very good article, thanks for writing.
Btw you spelled Sydney wrong in your profile.
Thank you, fixed :)
ConfigureAwait(false) is about not returning back to the synchronization context when an awaiter completes.
It's about performance, not deadlocks.
I believe I've covered that in the article in this section: