Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Should we use ConfigureAwait(false) in libraries that call async callbacks?

There are lots of guidelines for when to use ConfigureAwait(false), when using await/async in C#.

It seems the general recommendation is to use ConfigureAwait(false) in library code, as it rarely depends on the synchronization context.

However, assume we are writing some very generic utility code, which takes a function as input. A simple example could be the following (incomplete) functional combinators, to make simple task-based operations easier:

Map:

public static async Task<TResult> Map<T, TResult>(this Task<T> task, Func<T, TResult> mapping) {     return mapping(await task); } 

FlatMap:

public static async Task<TResult> FlatMap<T, TResult>(this Task<T> task, Func<T, Task<TResult>> mapping) {     return await mapping(await task); } 

The question is, should we use ConfigureAwait(false) in this case? I am unsure how the context capture works wrt. closures.

On one hand, if the combinators are used in a functional way, the synchronization context should not be necessary. On the other hand, people might misuse the API, and do context dependent stuff in the provided functions.

One option would be to have separate methods for each scenario (Map and MapWithContextCapture or something), but it feels ugly.

Another option might be to add the option to map/flatmap from and into a ConfiguredTaskAwaitable<T>, but as awaitables don't have to implement an interface this would result in a lot of redundant code, and in my opinion be even worse.

Is there a good way to switch the responsibility to the caller, such that the implemented library doesn't need to make any assumptions on whether or not the context is needed in the provided mapping-functions?

Or is it simply a fact, that async methods don't compose too well, without various assumptions?

EDIT

Just to clarify a few things:

  1. The problem does exist. When you execute the "callback" inside the utility function, the addition of ConfigureAwait(false) will result in a null sync. context.
  2. The main question is how we should tackle the situation. Should we ignore the fact that someone might want to use the sync. context, or is there a good way to shift the responsibility out to the caller, apart from adding some overload, flag or the like?

As a few answers mention, it would be possible to add a bool-flag to the method, but as I see it, this is not too pretty either, as it will have to be propagated all the way through the API's (as there are more "utility" functions, depending on the ones shown above).

like image 852
nilu Avatar asked May 04 '15 17:05

nilu


People also ask

Should I always use ConfigureAwait false?

As a general rule, every piece of code that is not in a view model and/or that does not need to go back on the main thread should use ConfigureAwait false. This is simple, easy and can improve the performance of an application by freeing the UI thread for a little longer.

When should I use ConfigureAwait true?

In this video we answer the ever popular question “Which do I use, ConfigureAwait True or False?”. The direct answer to this question is: – If you are a writing code for the UI, use ConfigureAwait(true).

Should I use ConfigureAwait false .NET Core?

There are very few use cases for the use of ConfigureAwait(true), it does nothing meaningful actually. In 99% of the cases, you should use ConfigureAwait(false). In . NET Framework by default the Task execution will continue on the captured context, this is ConfigureAwait(true).

What is ConfigureAwait () used for?

ConfigureAwait(false) configures the task so that continuation after the await does not have to be run in the caller context, therefore avoiding any possible deadlocks.


2 Answers

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.

like image 51
usr Avatar answered Sep 29 '22 08:09

usr


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.

like image 23
Yuval Itzchakov Avatar answered Sep 29 '22 06:09

Yuval Itzchakov