Should we use ConfigureAwait(false) in libraries that call async callbacks?
When you say await task.ConfigureAwait(false)
you transition to the thread-pool causing mapping
to run under a null context as opposed to running under the previous context. That can cause different behavior. So if the caller wrote:
await Map(0, i => { myTextBox.Text = i.ToString(); return 0; }); //contrived...
Then this would crash under the following Map
implementation:
var result = await task.ConfigureAwait(false);
return await mapper(result);
But not here:
var result = await task/*.ConfigureAwait(false)*/;
...
Even more hideous:
var result = await task.ConfigureAwait(new Random().Next() % 2 == 0);
...
Flip a coin about the synchronization context! This looks funny but it is not as absurd as it seems. A more realistic example would be:
var result =
someConfigFlag ? await GetSomeValue<T>() :
await task.ConfigureAwait(false);
So depending on some external state the synchronization context that the rest of the method runs under can change.
This also can happen with very simple code such as:
await someTask.ConfigureAwait(false);
If someTask
is already completed at the point of awaiting it there will be no switch of context (this is good for performance reasons). If a switch is necessary then the rest of the method will resume on the thread pool.
This non-determinism a weakness of the design of await
. It's a trade-off in the name of performance.
The most vexing issue here is that when calling the API is is not clear what happens. This is confusing and causes bugs.
What to do?
Alternative 1: You can argue that it is best to ensure deterministic behavior by always using task.ConfigureAwait(false)
.
The lambda must make sure that it runs under the right context:
var uiScheduler = TaskScheduler.FromCurrentSynchronizationContext;
Map(..., async x => await Task.Factory.StartNew(
() => { /*access UI*/ },
CancellationToken.None, TaskCreationOptions.None, uiScheduler));
It's probably best to hide some of this in a utility method.
Alternative 2: You can also argue that the Map
function should be agnostic to the synchronization context. It should just leave it alone. The context will then flow into the lambda. Of course, the mere presence of a synchronization context might alter the behavior of Map
(not in this particular case but in general). So Map
has to be designed to handle that.
Alternative 3: You can inject a boolean parameter into Map
that specifies whether to flow the context or not. That would make the behavior explicit. This is sound API design but it clutters the API. It seems inappropriate to concern a basic API such as Map
with synchronization context issues.
Which route to take? I think it depends on the concrete case. For example, if Map
is a UI helper function it makes sense to flow the context. If it is a library function (such as a retry helper) I'm not sure. I can see all alternatives make sense. Normally, it is recommended to apply ConfigureAwait(false)
in all library code. Should we make an exception in those cases where we call user callbacks? What if we have already left the right context e.g.:
void LibraryFunctionAsync(Func<Task> callback)
{
await SomethingAsync().ConfigureAwait(false); //Drops the context (non-deterministically)
await callback(); //Cannot flow context.
}
So unfortunately, there is no easy answer.
The question is, should we use ConfigureAwait(false) in this case?
Yes, you should. If the inner Task
being awaited is context aware and does use a given synchronization context, it would still be able to capture it even if whoever is invoking it is using ConfigureAwait(false)
. Don't forget that when disregarding the context, you're doing so in the higher level call, not inside the provided delegate. The delegate being executed inside the Task
, if needed, will need to be context aware.
You, the invoker, have no interest in the context, so it's absolutely fine to invoke it with ConfigureAwait(false)
. This effectively does what you want, it leaves the choice of whether the internal delegate will include the sync context up to the caller of your Map
method.
Edit:
The important thing to note is that once you use ConfigureAwait(false)
, any method execution after that would be on on an arbitrary threadpool thread.
A good idea suggested by @i3arnon would be to accept an optional bool
flag indicating whether context is needed or not. Although a bit ugly, would be a nice work around.
I think the real issue here comes from the fact that you are adding operations to Task
while you actually operate on the result of it.
There's no real reason to duplicate these operations for the task as a container instead of keeping them on the task result.
That way you don't need to decide how to await
this task in a utility method as that decision stays in the consumer code.
If Map
is instead implemented as follows:
public static TResult Map<T, TResult>(this T value, Func<T, TResult> mapping)
{
return mapping(value);
}
You can easily use it with or without Task.ConfigureAwait
accordingly:
var result = await task.ConfigureAwait(false)
var mapped = result.Map(result => Foo(result));
Map
here is just an example. The point is what are you manipulating here. If you are manipulating the task, you shouldn't await
it and pass the result to a consumer delegate, you can simply add some async
logic and your caller can choose whether to use Task.ConfigureAwait
or not. If you are operating on the result you don't have a task to worry about.
You can pass a boolean to each of these methods to signify whether you want to continue on the captured context or not (or even more robustly pass an options enum
flags to support other await
configurations). But that violates separation of concerns, as this doesn't have anything to do with Map
(or its equivalent).