“await task.ConfigureAwait(false)” versus “await ContextSwitcher.SwitchToThreadPool()” [closed]

允我心安 提交于 2020-01-06 05:36:06

问题


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 after await 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 than ContextSwitcher, 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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!