问题
It's widely recommended to use ConfigureAwait(false)
like this:
await Do1Async().ConfigureAwait(false);
// ...
await Do2Async().ConfigureAwait(false);
// ...
await Do3Async().ConfigureAwait(false);
// ...
IIRC, at the same time it's widely discouraged to use something like this ContextSwitcher, which would switch the async execution flow context to a pool thread and thus might help avoiding that ConfigureAwait
infestation across my method:
await ContextSwitcher.SwitchToThreadPool(); // this was even removed from async CTP
await Do1Async();
// ...
await Do2Async();
// ...
await Do3Async();
// ...
Why is the 1st option considered a good practice and this one isn't, especially given the fact the code after await Do1Async().ConfigureAwait(false)
will continue on exactly the same conditions as the code after await ContextSwitcher.SwitchToThreadPool()
?
Also, there is another alternative:
await Task.Run(async () => {
await Do1Async();
// ...
await Do2Async();
// ...
await Do3Async();
// ...
});
IIRC, this is still better than the ContextSwitcher
option, but why?
Finally, there is still this interesting approach: An alternative to ConfigureAwait(false) everywhere.
Here is the relevant part of SynchronizationContextRemover
from the author's repo:
public void OnCompleted(Action continuation)
{
var prevContext = SynchronizationContext.Current;
try
{
SynchronizationContext.SetSynchronizationContext(null);
continuation();
}
finally
{
SynchronizationContext.SetSynchronizationContext(prevContext);
}
}
Is it safe to just remove the synchronization context like that, which AFAIU would affect the whole synchronous scope after await new SynchronizationContextRemover()
?
await new SynchronizationContextRemover();
// we are still on the same thread
// but the synchronization context has been removed,
// be careful...
// ...
await Do1Async();
// ...until now
How is this SynchronizationContextRemover
better than ContextSwitcher
, besides perhaps it has one less switch to a pool thread?
回答1:
As others have noted, ConfigureAwait(false)
is less necessary with modern code (in particular, since ASP.NET Core has gone mainstream). Whether to use it in your library at this point is a judgement call; personally, I still do use it, but my main async library is very low-level.
especially given the fact the code after
await Do1Async().ConfigureAwait(false)
will continue on exactly the same conditions as the code afterawait ContextSwitcher.SwitchToThreadPool()
?
The conditions aren't exactly the same - there's a difference if Do1Async
completes synchronously.
Why is the 1st option considered a good practice and this one isn't
As explained by Stephen Toub, the "switcher" approach does allow code like this:
try
{
await Do1Async(); // UI thread
await ContextSwitcher.SwitchToThreadPool();
await Do2Async(); // Thread pool thread
}
catch (Exception)
{
... // Unknown thread
}
Specifically, catch
and finally
blocks can run in an unknown thread context, depending on runtime behavior. Code that can run on multiple threads is harder to maintain. This is the main reason it was cut from the Async CTP (also bearing in mind that with the language at that time, you couldn't await
in a catch
or finally
, so you couldn't switch to the desired context).
IIRC, this is still better than the
ContextSwitcher
option, but why?
I think it's important to note that these are all different semantics - in particular, they are all saying quite different things:
await x.ConfigureAwait(false)
says "I don't care what thread I resume on. It can be the same thread or a thread pool thread, whatever." Note: it does not say "switch to a thread pool thread."await ContextSwitcher()
says "Switch to this context and continue executing".await Task.Run(...)
says "Run this code on a thread pool thread and then resume me."
Out of all of those, I prefer the first and third. I use ConfigureAwait(false)
to say "this method doesn't care about the thread it resumes on", and I use Task.Run
to say "run this code on a background thread". I don't like the "switcher" approach because I find it makes the code less maintainable.
Is it safe to just remove the synchronization context like that, which AFAIU would affect the whole synchronous scope after
await new SynchronizationContextRemover()
?
Yes; the tricky part is that it needs to affect the synchronous scope. This is why there is no Func<Task>
or Func<Task<T>>
overloads for my SynchronizationContextSwitcher.NoContext method, which does pretty much the same thing. The one major difference between NoContext
and SynchronizationContextRemover
is that mine forces a scope (a lambda) in which there is no context and the remover is in the form of a "switcher". So again, mine forces the code to say "run this code without a context", whereas the switcher says "at this point in my method, remove the context, and then continue executing". In my opinion, the explicitly-scoped code is more maintainable (again, considering catch
/finally
blocks), which is why I use that style of API.
How is this
SynchronizationContextRemover
better thanContextSwitcher
, besides perhaps it has one less switch to a pool thread?
SynchronizationContextRemover
and NoContext
both stay on the same thread; they just temporarily remove the context on that thread. ContextSwitcher
does actually switch to a thread pool thread.
来源:https://stackoverflow.com/questions/59096962/await-task-configureawaitfalse-versus-await-contextswitcher-switchtothreadp