为什么是“SwitchTo"从异步CTP / Release中删除

本文关键字:CTP 异步 Release 删除 SwitchTo quot 为什么 | 更新日期: 2023-09-27 18:03:43

我今天尝试使用SwitchTo方法切换到GUI线程,并发现我将其从示例中解除不工作,仅仅是因为方法不存在。

然后我在这里找到了这个简介:

我们处理掉它的原因是因为它太危险了。另一种方法是将代码打包到TaskEx.Run…

我的问题很简单:为什么是危险的?使用它会导致哪些具体的危险?

请注意,我确实阅读了那篇文章的其余部分,所以我确实理解这里存在技术限制。我的问题仍然是,如果我意识到这一点,为什么是危险的?

我正在考虑重新实现助手方法来给我指定的功能,但是如果有一些根本的问题,除了有人认为它是危险的,我不会这样做。

特别地,非常天真地,以下是我如何考虑实现所需方法的:

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()
    {
    }
}

这将允许我写这样的代码:

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
}

为什么是“SwitchTo"从异步CTP / Release中删除

Stephen Toub在这个线程中有一些关于推理的更多信息。

总而言之,这不是一个好主意,原因有两个:

  1. 它提倡非结构化代码。如果您需要进行"繁重的处理",则应该将其放在Task.Run中。更好的是,将你的业务逻辑和你的UI逻辑分开。
  2. 错误处理和(某些)延续在未知上下文中运行。catch/finally块在Test将需要处理在线程池 UI上下文中运行(如果他们在线程池上下文中运行,他们不能使用SwitchTo跳到UI上下文中)。此外,只要您await返回的Task,您应该是OK的(await将在必要时纠正延续上下文),但是如果您使用ExecuteSynchronously显式ContinueWith延续,那么它们将有与catch/finally块相同的问题。

简而言之,没有SwitchTo,代码更干净,更可预测。

ConfigureAwait实际上比SwitchTo更危险。在心里跟踪当前上下文和最后一次SwitchTo调用并不比跟踪变量最后一次赋值的位置更困难。另一方面,当且仅当调用实际异步运行时,ConfigureAwait切换上下文。如果任务已经完成,则保留上下文。这是你无法控制的

现在是2020年,看起来SwitchTo很快就会回到CLR,根据David Fowler和Stephen Toub在这个GitHub问题,因为在try/catch中没有更多的await限制。

在我看来,明确地使用await TaskScheduler.Default.SwitchTo()比在第三方库代码中依赖ConfigureAwait(false)要好,特别是当我们想确保代码不会在任何自定义同步上下文中执行时。我有一篇博客文章详细介绍了这一点,包括SwitchTo的实验实现。

简而言之,我认为下面的第一个选项清楚地表明了意图,用最少的样板代码:

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

SwitchTo扩展方法在Microsoft.VisualStudio.Threading包中可用。下面是这个方法的签名:

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

下面是如何使用它的一个例子:

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
}