C# async/await chaining with ConfigureAwait(false)
Asked Answered
M

2

13

Based on numerous books and blogs including this excellent one here, it is clear that when one writes a dll library exposing helper async methods i.e. the wrapper methods, it is generally considered a best practice to internally complete the I/O task of actual async methods on a threadpool thread like so (pseudo code shown below for brevity and I'm using HttpClient as an example)

public Async Task<HttpResponseMessage> MyMethodAsync(..)
{
    ...
    var httpClient = new HttpClient(..);
    var response = await httpClient.PostAsJsonAsync(..).ConfigureAwait(false);
    ...
    return response;
}

The key here is the usage of ConfigureAwait(false) so that IO task completion occurs on a threadpool thread instead of on the original thread context, thereby potentially preventing deadlocks.

My question is from the perspective of a caller. I'm particularly interested in a scenario where there are layers of method calls between the caller and the above method call, as the following example shows.

CallerA -> Method1Async -> Method2Async -> finally the above MyMethodAsync

Is it enough to have ConfigureAwait(false) on the final method only or should one also ensure Method1Async and Method2Async also internally call their async methods with ConfigureAwait(false)? It seems silly to have it included on all these intermediary methods, especially if Method1Async and Method2Async are simply overloads that end up calling MyMethodAsync. Any thoughts, please enlighten us!

Updated with Example So if I have a library with the following private async method,

private async Task<string> MyPrivateMethodAsync(MyClass myClass)
{
    ...
    return await SomeObject.ReadAsStringAsync().ConfigureAwait(false);
}

should I make sure the following public overloaded methods both also include ConfigureAwait(false) as shown below?

public async Task<string> MyMethodAsync(string from)
{
        return await MyPrivateMethodAsync(new (MyClass() { From = from, To = "someDefaultValue"}).ConfigureAwait(false);
}
public async Task<string> MyMethodAsync(string from, string to)
{
        return await MyPrivateMethodAsync(new (MyClass() { From = from, To = to }).ConfigureAwait(false);
}
Murvyn answered 28/2, 2015 at 0:3 Comment(1)
While using ConfigureAwait(false) is a good practice for a library code, its use may have its own implications: https://mcmap.net/q/95081/-revisiting-task-configureawait-continueoncapturedcontext-falseUnexpressive
S
14

Definitely not. ConfigureAwait just as it's name suggest configures the await. It only affects the await coupled with it.

ConfigureAwait actually returns a different awaitable type, ConfiguredTaskAwaitable instead of Task which in turn returns a different awaiter type ConfiguredTaskAwaiter instead of TaskAwaiter

If you want to disregard the SynchronizationContext for all your awaits you must use ConfigureAwait(false) for each of them.

If you want to limit the use of ConfigureAwait(false) you can use my NoSynchronizationContextScope (see here) at the very top:

async Task CallerA()
{
    using (NoSynchronizationContextScope.Enter())
    {
        await Method1Async();
    }
}
Sika answered 28/2, 2015 at 0:11 Comment(6)
I think "must ... for each of them" is a bit too strict - is you have 2+ await in a row and first one really return asynchronously that the rest don't need ConfigureAwait(false) because you lost context when call returns... But in reality ConfigureAwait(false) should be on all calls (or none).Faizabad
@Sika a quick question on your NoSynchronizationContextScope which looks great and i would like to understand its use case. Going with my example above, r u saying that, with a tiny bit of extra coding to include your NoSynchronizationContextScope, CallerA can now instruct methods on the call tree i.e. Method1Async -> Method2Async -> all the way to MyMethodAsync to temporarily ignore SC regardless of whether or not these methods have ConfigureAwait(false) in them?Murvyn
@Murvyn pretty much. It's actually harsher than that. It temporarily removes the SC so inner methods don't have an SC to ignore.Sika
@i3arnon, aha got it thank u and makes sense! a practical usage scenario is that if library writer and end-users of library are 2 different entities who do not have access to each other's source code, then at least, each entity can still go ahead and have a final say as to whether SC should be avoided in their calls. u agree? also do u think library writer in general should expose async methods with an extra argument "useSC" defaulted to "false" and internally use ConfigureAwait(F) or not within if then blocks?Murvyn
@Murvyn I think that library developers should always use ConfigureAwait unless there's a specific reason not to (for example in a WPF library). The consumer should care about its own code and use ConfigureAwait if needed, but there's no reason to make that decision for the library.Sika
OK, I understand that ConfigureAwait only affects the await it is "coupled with". What if I have three async methods MethodA->MethodB->MethodC that are called in this order. MethodA is called from a WinForms app, MethodB uses ConfigureAwait(false) to await MethodC. But the library author of MethodC neglected to use ConfigureAwait(false). Will the "continuation" of MethodC run on a different SC than the call from MethodA because MethodB already "changed" the SC or will it run on the original SC and potentially still cause the issues we are trying to avoid with ConfigureAwait(false)?Lepton
C
5

When the task is awaited, it creates a corresponding TaskAwaiter to keep track of the task which also captures the current SynchronizationContext. After the task completes, the awaiter runs the code after the await ( called the continuation) on that captured context.

You can prevent that by calling ConfigureAwait(false), which creates a different kind of awiatable (ConfiguredTaskAwaitable) and its corresponding awaiter (ConfiguredTaskAwaitable.ConfiguredTaskAwaiter) that does not run the continuation on the captured context.

The point is that for each await, a different instance of an awaiter is created, it is not something that is shared between all the awaitables in the method or program. So it's best that you call ConfigureAwait(false) for each await statement.

You can see the source code for the awaiters here.

Chambertin answered 28/2, 2015 at 0:15 Comment(6)
A minor correction: when a Task is created it does not captures the current SynchronizationContext. When Task.GetAwaiter() is called (by the compiler-generated code for await), at that point SynchronizationContext gets captured.Unexpressive
@Noseratio thanks for your comment. I got this from the Task source. Task.GetAwaiter() doesn't seem to do it, although I thought it did. I must be missing somethingChambertin
I didn't say TaskAwaiter captures SC when it gets created. It does that in its implementation of ICriticalNotifyCompletion.OnCompleted/UnsafeOnCompleted here, which is the key method of any awaiter.Unexpressive
I see now, look like OnCompleted does not get called after the Task is completed :)Chambertin
Actually, UnsafeOnCompleted gets called synchronously exactly at the await point, to store the continuation callback before the task starts its asynchronous logic. It's then the responsibility of the TaskAwaiter to call this callback when the task completes.Unexpressive
Also note, "does not run the continuation on the captured context" can result in "pushes it to ThreadPool", which may not be always obvious or desirable. Check this and this.Unexpressive

© 2022 - 2024 — McMap. All rights reserved.