16

I tried to use the SwitchTo method today to switch to the GUI thread, and found that the example I lifted it from does not work, simply because the method is not there.

I then found this blurb here:

The reason we got rid of it was because it was so dangerous. The alternative is to bundle up your code inside TaskEx.Run...

My question is simply: Why was it dangerous? What specific dangers would using it lead to?

Note that I did read the rest of that post, so I do understand there are technical limitations here. My question is still, if I'm aware of this, why is it dangerous?

I am considering reimplementing helper methods to give me the specified functionality, but if there is something fundamentally broken, other than that someone decided it was dangerous, I would not do it.

Specifically, very naively, here's how I would consider implementing the required methods:

public static class ContextSwitcher
{
    public static ThreadPoolContextSwitcher SwitchToThreadPool()
    {
        return new ThreadPoolContextSwitcher();
    }

    public static SynchronizationContextSwitcher SwitchTo(this SynchronizationContext synchronizationContext)
    {
        return new SynchronizationContextSwitcher(synchronizationContext);
    }
}

public class SynchronizationContextSwitcher : INotifyCompletion
{
    private readonly SynchronizationContext _SynchronizationContext;

    public SynchronizationContextSwitcher(SynchronizationContext synchronizationContext)
    {
        _SynchronizationContext = synchronizationContext;
    }

    public SynchronizationContextSwitcher GetAwaiter()
    {
        return this;
    }

    public bool IsCompleted
    {
        get
        {
            return false;
        }
    }

    public void OnCompleted(Action action)
    {
        _SynchronizationContext.Post(_ => action(), null);
    }

    public void GetResult()
    {
    }
}

public class ThreadPoolContextSwitcher : INotifyCompletion
{
    public ThreadPoolContextSwitcher GetAwaiter()
    {
        return this;
    }

    public bool IsCompleted
    {
        get
        {
            return false;
        }
    }

    public void OnCompleted(Action action)
    {
        ThreadPool.QueueUserWorkItem(_ => action(), null);
    }

    public void GetResult()
    {
    }
}

This would allow me to write code like this:

public async void Test()
{
    await ContextSwitcher.SwitchToThreadPool(); // ensure we're not bogging down the UI thread
    // do some heavy processing
    await _UIContext.SwitchTo(); // presumably saved from the main thread
    // update UI with new data
}
svick
  • 236,525
  • 50
  • 385
  • 514
Lasse V. Karlsen
  • 380,855
  • 102
  • 628
  • 825
  • Hah. Now that's quite an old thread! I never have been a fan of Microsoft's occasional "it's for your own good" reasoning. – Cory Nelson Mar 12 '13 at 14:37
  • 2
    I have since switched to doing `await Task.Run(async ()=>{})` -- not to avoid some vacuous dangers, but simply because I think it's easier to read. I do think your idea of how to implement `SwitchTo()` is sound, though. – Cory Nelson Mar 12 '13 at 14:37
  • Didn't know about the `async ()=>{}` syntax, needs further investigation, thanks! – Lasse V. Karlsen Mar 12 '13 at 14:46
  • I would suggest using `Send()` rather than `Post()`, because according to [this blog post](https://msdn.microsoft.com/magazine/gg598924.aspx), this will mean, for the `DispatcherSynchronizationContext`, that it's precisely the non-ui threads that will switch to the ui thread and the ui thread will keep the context. With a `Post()`, an await would make a detour through the dispatcher messing up your stack trace even if you await on the ui thread. And for all other `SynchronizationContexts` it wouldn't matter anyway. (They'd be non-changing then, which is ok too I think.) – John Jun 24 '17 at 22:36

4 Answers4

9

Stephen Toub has some more information on the reasoning in this thread.

To summarize, it's not a good idea for two reasons:

  1. It promotes unstructured code. If you have "heavy processing" that you need to do, it should be placed in a Task.Run. Even better, separate your business logic from your UI logic.
  2. Error handling and (some) continuations run in an unknown context. catch/finally blocks in Test would need to handle running in a thread pool or UI context (and if they're running in the thread pool context, they can't use SwitchTo to jump on the UI context). Also, as long as you await the returned Task you should be OK (await will correct the continuation context if necessary), but if you have explicit ContinueWith continuations that use ExecuteSynchronously, then they'll have the same problem as the catch/finally blocks.

In short, the code is cleaner and more predictable without SwitchTo.

Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Ok, that sheds more light on it. It still looks odd, that post, to say it creates all sorts of problems with unknown contexts, and then moves on to show how to implement the support anyway. – Lasse V. Karlsen Mar 12 '13 at 15:06
  • 1
    @LasseV.Karlsen I think that attitude is not uncommon for the developers of .Net. As I understand it, they're saying something like “If we provided this feature, people would think it's okay to use it, so they would use it often, which is a bad thing. But if it's posted on some blog, they are likely to use it only when they really need it and understand what's really going on, which is okay.” – svick Mar 13 '13 at 00:37
  • @BrunoMartinez: That's completely different. `SwitchTo` would cause serious problems with ambiguous contexts; the number of arguments passed to a method doesn't cause problems like that. Also, `SwitchTo` was about just not providing a dangerous API; by removing it from the API, they're reducing the complexity of what they have to provide. Restricting the number of method arguments is *adding* functionality, and would actually create a [whole slew of work](http://blogs.msdn.com/b/ericlippert/archive/2011/03/03/danger-will-robinson.aspx). – Stephen Cleary Feb 06 '14 at 22:31
  • 1
    @StephenCleary I think you meant to reply to my answer below. – Bruno Martinez Aug 19 '14 at 20:38
  • One case I used `SwitchTo` - is a wrapper I wrote for one 3rd-party proprietary library, which must be initialized and invoked in a single thread, otherwise it caused crashes. Like this: `async Task WrappedMethod() { await dedicatedContext.SwitchTo(); UnsafeMethod(); }`. Since not all the methods were one-liners and could contain awaits in the middle, code with `SwitchTo` looked better then code with `Task.Run`. – stop-cran Oct 29 '21 at 08:21
5

ConfigureAwait is actually more dangerous than SwitchTo. Mentally tracking the current context and the last SwitchTo call is no more difficult than tracking where a variable was last assigned. On the other hand, ConfigureAwait switches context if and only if the call actually ran asynchronously. If the task was already completed, the context is preserved. You have no control over this.

Bruno Martinez
  • 2,850
  • 2
  • 39
  • 47
2

It's 2020 and it looks like SwitchTo is set to come back to CLR soon, according to David Fowler and Stephen Toub in this GitHub issue, as there's no more limitations for await inside try/catch.

IMO, using something like await TaskScheduler.Default.SwitchTo() explicitly is better than relying upon ConfigureAwait(false) in the 3rd party library code, especially if we want to make sure that code doesn't execute on any custom synchronization context. I have a blog post with more details on that, including an experimental implementation of SwitchTo.

In a nutshell, I believe the first option from the below clearly indicates the intent, with minimum boilerplate code:

// switch to the thread pool explicitly for the rest of the async method
await TaskScheduler.Default.SwitchTo();
await RunOneWorkflowAsync();
await RunAnotherWorkflowAsync();
// execute RunOneWorkflowAsync on the thread pool 
// and stay there for the rest of the async method
await Task.Run(RunOneWorkflowAsync).ConfigureAwait(false);
await RunAnotherWorkflowAsync();
await Task.Run(async () => 
{
  // start on the thread pool
  await RunOneWorkflowAsync();
  await RunAnotherWorkflowAsync();
}).ConfigureAwait(false);
// continue on the thread pool for the rest of the async method
// start on whatever the current synchronization context is
await RunOneWorkflowAsync().ConfigureAwait(false);
// continue on the thread pool for the rest of the async method,
// unless everything inside `RunOneWorkflowAsync` has completed synchronously
await RunAnotherWorkflowAsync();
noseratio
  • 59,932
  • 34
  • 208
  • 486
  • 1
    I am not sure that the `await Task.Run(RunOneWorkflowAsync).ConfigureAwait(false);` guarantees that the rest of the async method will run on the thread pool. Isn't it possible for a thread-switch (imposed by the operating system) to occur just after creating the task and just before awaiting it, so that when the thread resumes to find that the task has already been completed? In that case the `ConfigureAwait(false)` will have no effect, because it only affects the awaiting of non-completed tasks. For example `await Task.CompletedTask.ConfigureAwait(false);` has no effect. – Theodor Zoulias Sep 28 '20 at 10:09
  • @TheodorZoulias, that's certainly may be a problem for `await RunOneWorkflowAsync().ConfigureAwait(false)`. If everything inside `RunOneWorkflowAsync` completes synchronously, we'll continue synchronously on the original context. – noseratio Sep 28 '20 at 10:42
  • 1
    As to `await Task.Run(RunOneWorkflowAsync).ConfigureAwait(false)`, my guts tell me it will always continue on a `ThreadPool`, but I have to verify that, I'll be back with the results :) – noseratio Sep 28 '20 at 10:42
  • I haven't tried to prove that it's possible, and may not be possible for some internal reason. I would try to test it by starting hundreds of threads, so that the operating system has to switch frequently from thread to thread. – Theodor Zoulias Sep 28 '20 at 11:02
  • @TheodorZoulias so with `Task.Run(RunOneWorkflowAsync)`, it is expected to schedule the `RunOneWorkflowAsync` call on thread pool ([here is how, `TaskCreationOptions.LongRunning` doesn't apply](https://source.dot.net/System.Private.CoreLib/R/33cd274e06874569.html)). In theory, it may happen to be the same thread that called `Task.Run` (say if the calling method completed briefly and that thread was returned to the pool *before* our task gets de-queued). Yet it'd still be a queued async continuation, i.e., it woudn't happen the same stack frame. – noseratio Sep 28 '20 at 11:06
  • ... in which case, even if there was a synchronization context on that pool thread, `ConfigureAwait(false)` [would re-queue the continuation to another pool thread](https://stackoverflow.com/questions/22672984/configureawait-pushes-the-continuation-to-a-pool-thread). Thinking about all these corner cases is mind-twising, that's one other reason why I like the simplicity of `await TaskScheduler.Default.SwitchTo()` :) – noseratio Sep 28 '20 at 11:07
  • 1
    Yeap, I would not feel confident about the switch without an awaiter with a hard-coded `false` as the return value of its [`IsCompleted`](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.compilerservices.yieldawaitable.yieldawaiter.iscompleted) property. :-) – Theodor Zoulias Sep 28 '20 at 11:12
  • 1
    @TheodorZoulias that's exactly what I have `alwaysSchedule` param for :) https://gist.github.com/noseratio/5d2d5f2a0cbb71b7880ce731c3958e62#file-taskschedulerawaiter-cs-L53 – noseratio Sep 28 '20 at 13:05
  • Any updates on this from you guys who are in the thick of it? Would a single await Task.Run(() => {int test = 1;}).ConfigureAwait(false); effectively achieve the same as the "SwitchTo" ? You could then use Dispatcher.InvokeAsync if you need to go back to the UI thread. Is there anything I've misunderstood? – rollsch Jun 20 '21 at 05:33
  • @rolls, not exactly. `await TaskScheduler.Default.SwitchTo()` might be a no-op if it's already a pool thread without any sync context. `Task.Run` will normally queue a task (in simple terms, think `ThreadPool.QueueUserWorkItem`), plus other minor overhead for allocating a task etc. I.e., a proper implementation of `SwitchTo` would be a bit more efficient. – noseratio Jun 20 '21 at 09:45
  • Also, I personally don't like the idea of switching back and force between thread pool and UI thread within the same method. I'd rather use `await TaskScheduler.Default.SwitchTo()` as a tool to make sure the rest of my `async` method executes on thread pool. – noseratio Jun 20 '21 at 09:47
  • If you have an iprogress that must execute on the UI thread and a very slow method that you run on a background thread how do you do that without constantly context switching? That is the problem I have. – rollsch Jun 20 '21 at 10:51
  • @rolls You don't need `Dispatcher.InvokeAsync` for `IProgress` pattern but yeah, I'd make an exception for the progress notifications ;) Keep in mind though, `progress.Report(i)` is not awaitable but it's still async, it uses `SynchronizationContext.Post`. I have an old related answer: https://stackoverflow.com/questions/21611292/cancel-thread-and-restart-it/21639075#21639075 – noseratio Jun 20 '21 at 12:59
0

The SwitchTo extension method is available in the Microsoft.VisualStudio.Threading package. Here is the signature of this method:

public static
    Microsoft.VisualStudio.Threading.AwaitExtensions.TaskSchedulerAwaitable
    SwitchTo(this System.Threading.Tasks.TaskScheduler scheduler,
    bool alwaysYield = false);

And here is an example of how to use it:

using Microsoft.VisualStudio.Threading;

private async void Button_Click(object sender, EventArgs e) 
{
    var ui = TaskScheduler.FromCurrentSynchronizationContext(); // Capture the UI thread

    // Do something on the UI thread

    await TaskScheduler.Default.SwitchTo(); // Switch to the ThreadPool

    // Do something on the ThreadPool

    await ui.SwitchTo(); // Switch back to the UI thread

    // Do something on the UI thread
}
Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104