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);
}
SamDevx
  • 2,268
  • 4
  • 32
  • 47
  • While using `ConfigureAwait(false)` is a good practice for a library code, its use may have its own implications: http://stackoverflow.com/q/28410046 – noseratio Feb 28 '15 at 06:27

2 Answers2

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();
    }
}
i3arnon
  • 113,022
  • 33
  • 324
  • 344
  • 1
    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). – Alexei Levenkov Feb 28 '15 at 00:47
  • 1
    @i3arnon 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? – SamDevx Feb 28 '15 at 17:22
  • 1
    @SamDevx pretty much. It's actually harsher than that. It temporarily removes the SC so inner methods don't have an SC to ignore. – i3arnon Feb 28 '15 at 17:26
  • @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? – SamDevx Feb 28 '15 at 17:50
  • @SamDevx 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. – i3arnon Feb 28 '15 at 17:57
  • 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)`? – Rev Sep 22 '22 at 12:21
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.

NeddySpaghetti
  • 13,187
  • 5
  • 32
  • 61
  • 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. – noseratio Feb 28 '15 at 10:20
  • @Noseratio thanks for your comment. I got this from the [Task source](http://referencesource.microsoft.com/#mscorlib/system/runtime/compilerservices/TaskAwaiter.cs,16e40fc537484d93). `Task.GetAwaiter()` doesn't seem to do it, although I thought it did. I must be missing something – NeddySpaghetti Mar 01 '15 at 03:18
  • I didn't say `TaskAwaiter` captures SC when it gets created. It does that in its implementation of `ICriticalNotifyCompletion.OnCompleted/UnsafeOnCompleted` [here](http://referencesource.microsoft.com/#mscorlib/system/runtime/compilerservices/TaskAwaiter.cs,45f10a20f8fdfd61,references), which is the key method of any awaiter. – noseratio Mar 01 '15 at 03:21
  • I see now, look like `OnCompleted` does not get called after the `Task` is completed :) – NeddySpaghetti Mar 01 '15 at 06:30
  • 1
    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. – noseratio Mar 01 '15 at 06:36
  • 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](http://stackoverflow.com/questions/22672984/configureawait-pushes-the-continuation-to-a-pool-thread) and [this](http://stackoverflow.com/questions/28410046/revisiting-task-configureawaitcontinueoncapturedcontext-false). – noseratio Mar 01 '15 at 06:48