Why was "SwitchTo" removed from Async CTP / Release?
Asked Answered
W

4

17

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
}
Whether answered 12/3, 2013 at 14:5 Comment(4)
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.Slotnick
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.Slotnick
Didn't know about the async ()=>{} syntax, needs further investigation, thanks!Whether
I would suggest using Send() rather than Post(), because according to this blog post, 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.)Anabiosis
D
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.

Doeskin answered 12/3, 2013 at 14:56 Comment(5)
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.Whether
@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.”Forensics
@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.Doeskin
@StephenCleary I think you meant to reply to my answer below.Hizar
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.Allegory
H
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.

Hizar answered 6/2, 2014 at 22:38 Comment(0)
P
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();
Purine answered 28/9, 2020 at 8:23 Comment(13)
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.Newcomb
@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.Purine
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 :)Purine
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.Newcomb
@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). 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.Purine
... 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. Thinking about all these corner cases is mind-twising, that's one other reason why I like the simplicity of await TaskScheduler.Default.SwitchTo() :)Purine
Yeap, I would not feel confident about the switch without an awaiter with a hard-coded false as the return value of its IsCompleted property. :-)Newcomb
@TheodorZoulias that's exactly what I have alwaysSchedule param for :) gist.github.com/noseratio/…Purine
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?Heraldic
@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.Purine
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.Purine
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.Heraldic
@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: #21611792Purine
N
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
}
Newcomb answered 27/1, 2022 at 0:10 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.