Question

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
}
Était-ce utile?

La solution

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.

Autres conseils

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.

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();

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
}
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top