Revisiting Task.ConfigureAwait(continueOnCapturedContext: false)

When you're dealing with asynchronous operations, the overhead of a thread switch is way too small to care about (generally speaking). The purpose of ConfigureAwait(false) is not to induce a thread switch (if necessary), but rather to prevent too much code running on a particular special context.

The reasoning behind this pattern was that "it helps to avoid deadlocks".

And stack dives.

But I do think this is a non-problem in the general case. When I encounter code that doesn't properly use ConfigureAwait, I just wrap it in a Task.Run and move on. The overhead of thread switches isn't worth worrying about.


The major design goal behind ConfigureAwait(false) is to reduce redundant SynchronizationContext.Post continuation callbacks for await, where possible. This usually means less thread switching and less work on the UI threads.

I disagree with your premise. ConfigureAwait(false) goal is to reduce, as much as possible, the work that needs to be marshalled back to "special" (e.g. UI) contexts in spite of the thread switches it may require off of that context.

If the goal was to reduce thread switches you could just remain in the same special context throughout all the work, and then no other threads are required.

To achieve that you should be using ConfigureAwait everywhere you don't care about the thread executing the continuation. If you take your example and use ConfigureAwait appropriately you would only get a single switch (instead of 2 without it):

private async void Button_Click(object sender, RoutedEventArgs e)
{
    TaskExt.Log("A1");
    await AnotherClass.MethodAsync().ConfigureAwait(false);
    TaskExt.Log("A2");
}

public class AnotherClass
{
    public static async Task MethodAsync()
    {
        TaskExt.Log("B1");
        await SomeClass.SomeAsyncApi().ConfigureAwait(false);
        TaskExt.Log("B2");
    }
}

public class SomeClass
{
    public static async Task<int> SomeAsyncApi()
    {
        TaskExt.Log("X1");
        await Task.Delay(1000).WithCompletionLog(step: "X1.5").ConfigureAwait(false);
        TaskExt.Log("X2");
        return 42;
    }
}

Output:

{ step = A1, thread = 9 }
{ step = B1, thread = 9 }
{ step = X1, thread = 9 }
{ step = X1.5, thread = 11 }
{ step = X2, thread = 11 }
{ step = B2, thread = 11 }
{ step = A2, thread = 11 }

Now, where you do care about the continuation's thread (e.g. when you use UI controls) you "pay" by switching to that thread, by posting the relevant work to that thread. You've still gained from all the work that didn't require that thread.

If you want to take it even further and remove the synchronous work of these async methods from the UI thread you only need to use Task.Run once, and add another switch:

private async void Button_Click(object sender, RoutedEventArgs e)
{
    TaskExt.Log("A1");
    await Task.Run(() => AnotherClass.MethodAsync()).ConfigureAwait(false);
    TaskExt.Log("A2");
}

Output:

{ step = A1, thread = 9 }
{ step = B1, thread = 10 }
{ step = X1, thread = 10 }
{ step = X1.5, thread = 11 }
{ step = X2, thread = 11 }
{ step = B2, thread = 11 }
{ step = A2, thread = 11 }

This guideline to use ConfigureAwait(false) is directed at library developers because that's where it actually matters, but the point is to use it whenever you can and in that case you reduce the work on these special contexts while keeping thread switching at a minimum.


Using WithNoContext has exactly the same outcome as using ConfigureAwait(false) everywhere. The cons however is that it messes with the thread's SynchronizationContext and that you aren't aware of that inside the async method. ConfigureAwait directly affects the current await so you have the cause and effect together.

Using Task.Run too, as I've pointed out, has exactly the same outcome of using ConfigureAwait(false) everywhere with the added value of offloading the synchronous parts of the async method to the ThreadPool. If this is needed, then Task.Run is appropriate, otherwise ConfigureAwait(false) is enough.


Now, If you're dealing with a buggy library when ConfigureAwait(false) isn't used appropriately, you can hack around it by removing the SynchronizationContext but using Thread.Run is much simpler and clearer and offloading work to the ThreadPool has a very negligible overhead.