Too long to read. Using Task.ConfigureAwait(continueOnCapturedContext: false)
may be introducing redundant thread switching. I'm looking for a consistent solution to that.
Long version. 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. However, it isn't always how it works.
For example, there is a 3rd party library implementing SomeAsyncApi
API. Note that ConfigureAwait(false)
is not used anywhere in this library, for some reason:
// some library, SomeClass class
public static async Task<int> SomeAsyncApi()
{
TaskExt.Log("X1");
// await Task.Delay(1000) without ConfigureAwait(false);
// WithCompletionLog only shows the actual Task.Delay completion thread
// and doesn't change the awaiter behavior
await Task.Delay(1000).WithCompletionLog(step: "X1.5");
TaskExt.Log("X2");
return 42;
}
// logging helpers
public static partial class TaskExt
{
public static void Log(string step)
{
Debug.WriteLine(new { step, thread = Environment.CurrentManagedThreadId });
}
public static Task WithCompletionLog(this Task anteTask, string step)
{
return anteTask.ContinueWith(
_ => Log(step),
CancellationToken.None,
TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.Default);
}
}
Now, let's say there's some client code running on a WinForms UI thread and using SomeAsyncApi
:
// another library, AnotherClass class
public static async Task MethodAsync()
{
TaskExt.Log("B1");
await SomeClass.SomeAsyncApi().ConfigureAwait(false);
TaskExt.Log("B2");
}
// ...
// a WinFroms app
private async void Form1_Load(object sender, EventArgs e)
{
TaskExt.Log("A1");
await AnotherClass.MethodAsync();
TaskExt.Log("A2");
}
The output:
{ step = A1, thread = 9 } { step = B1, thread = 9 } { step = X1, thread = 9 } { step = X1.5, thread = 11 } { step = X2, thread = 9 } { step = B2, thread = 11 } { step = A2, thread = 9 }
Here, the logical execution flow goes through 4 thread switches. 2 of them are redundant and caused by SomeAsyncApi().ConfigureAwait(false)
. It happens because ConfigureAwait(false)
pushes the continuation to ThreadPool
from a thread with synchronization context (in this case, the UI thread).
In this particular case, MethodAsync
is better off without ConfigureAwait(false)
. Then it only takes 2 thread switches vs 4:
{ step = A1, thread = 9 } { step = B1, thread = 9 } { step = X1, thread = 9 } { step = X1.5, thread = 11 } { step = X2, thread = 9 } { step = B2, thread = 9 } { step = A2, thread = 9 }
However, the author of MethodAsync
uses ConfigureAwait(false)
with all good intentions and following the best practices, and she knows nothing about internal implementation of SomeAsyncApi
. It wouldn't be a problem if ConfigureAwait(false)
was used "all the way" (i.e., inside SomeAsyncApi
too), but that's beyond her control.
That's how it goes with WindowsFormsSynchronizationContext
(or DispatcherSynchronizationContext
), where we might be not caring about extra thread switches at all. However, a similar situation could happen in ASP.NET, where AspNetSynchronizationContext.Post
essentially does this:
Task newTask = _lastScheduledTask.ContinueWith(_ => SafeWrapCallback(action));
_lastScheduledTask = newTask;
The whole thing may look as a contrived issue, but I did see a lot of production code like this, both client-side and server-side. Another questionable pattern I came across: await TaskCompletionSource.Task.ConfigureAwait(false)
with SetResult
being called on the same synchronization context as that captured for the former await
. Again, the continuation was redundantly pushed to ThreadPool
. The reasoning behind this pattern was that "it helps to avoid deadlocks".
The question: In the light of the described behavior of ConfigureAwait(false)
, I'm looking for an elegant way of using async/await
while still minimizing redundant thread/context switching. Ideally, something that would work existing 3rd party libraries.
What I've looked at, so far:
Offloading an
async
lambda withTask.Run
is not ideal as it introduces at least one extra thread switch (although it can potentially save many others):await Task.Run(() => SomeAsyncApi()).ConfigureAwait(false);
One other hackish solution might be to temporarily remove synchronization context from the current thread, so it won't be captured by any subsequent awaits in the inner chain of calls (I previously mentioned it here):
async Task MethodAsync() { TaskExt.Log("B1"); await TaskExt.WithNoContext(() => SomeAsyncApi()).ConfigureAwait(false); TaskExt.Log("B2"); }
{ step = A1, thread = 8 } { step = B1, thread = 8 } { step = X1, thread = 8 } { step = X1.5, thread = 10 } { step = X2, thread = 10 } { step = B2, thread = 10 } { step = A2, thread = 8 }
public static Task<TResult> WithNoContext<TResult>(Func<Task<TResult>> func) { Task<TResult> task; var sc = SynchronizationContext.Current; try { SynchronizationContext.SetSynchronizationContext(null); // do not await the task here, so the SC is restored right after // the execution point hits the first await inside func task = func(); } finally { SynchronizationContext.SetSynchronizationContext(sc); } return task; }
This works, but I don't like the fact that it tampers with the thread's current synchronization context, albeit for a very short scope. Moreover, there's another implication here: in the absence of
SynchronizationContext
on the current thread, an ambientTaskScheduler.Current
will be used forawait
continuations. To account for this,WithNoContext
could possibly be altered like below, which would make this hack even more exotic:// task = func(); var task2 = new Task<Task<TResult>>(() => func()); task2.RunSynchronously(TaskScheduler.Default); task = task2.Unwrap();
I'd appreciate any other ideas.
Updated, to address @i3arnon's comment:
I would say that it's the other way around because as Stephen said in his answer "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." which you disagree with and is the root of your compliant.
As your answer has been edited, here is your statement I disagreed with, for clarity:
ConfigureAwait(false) goal is to reduce, as much as possible, the work the "special" (e.g. UI) threads need to process in spite of the thread switches it requires.
I also disagree with your current version of that statement. I'll refer you to the primary source, Stephen Toub's blog post:
Avoid Unnecessary Marshaling
If at all possible, make sure the async implementation you’re calling doesn’t need the blocked thread in order to complete the operation (that way, you can just use normal blocking mechanisms to wait synchronously for the asynchronous work to complete elsewhere). In the case of async/await, this typically means making sure that any awaits inside of the asynchronous implementation you’re calling are using ConfigureAwait(false) on all await points; this will prevent the await from trying to marshal back to the current SynchronizationContext. As a library implementer, it’s a best practice to always use ConfigureAwait(false) on all of your awaits, unless you have a specific reason not to; this is good not only to help avoid these kinds of deadlock problems, but also for performance, as it avoids unnecessary marshaling costs.
It does says that the goal is to avoid unnecessary marshaling costs, for performance. A thread switch (which flows the ExecutionContext
, among other things) is a big marshaling cost.
Now, it doesn't say anywhere that the goal is to reduce the amount of work which is done on "special" threads or contexts.
While this may make certain sense for UI threads, I still don't think it is the major goal behind ConfigureAwait
. There are other - more structured - ways to minimize work on UI threads, like using chunks of await Task.Run(work)
.
Moreover, it doesn't make sense at all to minimize work on AspNetSynchronizationContext
- which itself flows from thread to thread, unlike with a UI thread. Quite opposite, once you're on AspNetSynchronizationContext
, you want to make as much work as possible, to avoid unnecessary switching in the middle of handling the HTTP request. Nevertheless, it still makes perfect sense to use ConfigureAwait(false)
in ASP.NET: if used correctly, it again reduces the server-side thread switching.
ConfigureAwait
). The difference is which threads are involved. – HenryhenryettaConfigureAwait(false)
, and all continuations are posted asynchronously toWindowsFormsSynchronizationContext
, from the UI thread to the same UI thread (via the message loop). Besides the one posted fromThreadPool
thread (whereTask.Delay
ended) to the UI thread, that's a thread switch. Now, addConfigureAwait(false)
back and see one moreThreadPool
involved, totally redundant. I.e., the difference is how many additional threads gets involved in the async workflow. – Dhaulagiri11
. Once it servers the completion ofTask.Delay
, then it returns to the pool, and then again it happens to serve the continuation ofConfigureAwait(false)
. BecauseSomeAsyncApi()
task completes on the UI thread9
which has an SC and that's whereConfiguredTaskAwaitable
pushes the continuation to a now-available pool thread11
. That's what I mean under redundant switch, and I think I'm failing to explain it any better. – DhaulagiriThreadPool
thread throughout without switching you can also have many of those. What you should be wary about is marshalling continuations back to the UI thread when this isn't needed. – HenryhenryettaConfigureAwait(false)
and - if absolutely necessary - useTaskRun(() => SomethingAsync())
to hope off the synchronization context. – DhaulagiriTask
-based API, possibly in a closed-source form, and it is causing you problems like dead-locks or UI stuttering. By not relying unconsciously upon usingConfigureAwait(false)
throughout your own code, you could be in a better position to find and isolate those "offending" APIs. – DhaulagiriConfigureAwait(false)
could cause more thread switching. :-) – Gallagerawait TaskScheduler.Default.SwitchTo()
explicitly for that. – DhaulagiriTaskScheduler.SwitchTo()
concept too. Btw I gave up at trying to create the counterexample. It's not easy, and may not be even possible. :-) – Gallagerawait Task.Delay(5000).RestoreContext().ForceAsync()
, the comment link. – Dhaulagiriawait t.ConfigureAwait(AwaitBehavior.NoThrow | AwaitBehavior.ForceAsync);
, since this functionality will probably not be needed too often. It's sad that they decided not to implement any of this, except from theWaitAsync(token)
functionality. – GallagerConfigureAwait(false)
, couldB2
have been on a thread other than 11? Or does it have to run on the same thread as whatever threadX1.5
is run on? – ConvergeConfigureAwait(false)
and because ofTaskContinuationOptions.ExecuteSynchronously
, it'd be the same thread nearly always (#11 in that case). I say "nearly" because there're very rare edge cases were it might not, at least for .NET 4.x: devblogs.microsoft.com/pfxteam/… Not sure if anything has changed in .NET Core/5+. – DhaulagiriConfigureAwait
: dev.to/noseratio/why-i-no-longer-use-configureawait-false-3pne. Edited: oh, I think we talked there before 🙂 – Dhaulagiri