11

我刚刚注意到,对于 .NET 4.5,每个Dispatcher.BeginInvoke/InvokeAsync回调都是在其自己非常独特的同步上下文(的实例DispatcherSynchronizationContext)上执行的。这种变化背后的原因是什么?

以下简单的 WPF 应用程序说明了这一点:

using System;
using System.Diagnostics;
using System.Threading;
using System.Windows;
using System.Windows.Threading;

namespace WpfApplication
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            Action test = null;
            var i = 0;

            test = () =>
            {
                var sc = SynchronizationContext.Current;

                Dispatcher.CurrentDispatcher.InvokeAsync(() => 
                {
                    Debug.Print("same context #" + i + ": " +
                        (sc == SynchronizationContext.Current));
                    if ( i < 10 ) 
                    {
                        i++;
                        test();
                    }
                });
            };

            this.Loaded += (s, e) => test();
        }
    }
}

输出:

相同的上下文#0:错误
相同的上下文#1:错误
相同的上下文#2:错误
...

设置BaseCompatibilityPreferences.ReuseDispatcherSynchronizationContextInstancetrue恢复 .NET 4.0 行为:

public partial class App : Application
{
    static App()
    {
        BaseCompatibilityPreferences.ReuseDispatcherSynchronizationContextInstance = true;
    }
}
相同的上下文#0:真
相同的上下文#1:真
相同的上下文#2:真
...

研究.NET 源代码表明DispatcherOperation

[SecurityCritical]
private void InvokeImpl() 
{
    SynchronizationContext oldSynchronizationContext = SynchronizationContext.Current;

    try 
    {
        // We are executing under the "foreign" execution context, but the 
        // SynchronizationContext must be for the correct dispatcher. 
        SynchronizationContext.SetSynchronizationContext(new DispatcherSynchronizationContext(_dispatcher));

        // Invoke the delegate that does the work for this operation.
        _result = _dispatcher.WrappedInvoke(_method, _args, _isSingleParameter);
    }
    finally 
    {
        SynchronizationContext.SetSynchronizationContext(oldSynchronizationContext); 
    } 
}

我不明白为什么可能需要这样做,无论如何排队的回调Dispatcher.BeginInvoke/InvokeAsync都在正确的线程上执行,该线程上已经DispatcherSynchronizationContext安装了一个实例。

此更改的一个有趣的副作用是.NET 4.5 WPF 中await TaskCompletionSource.Task的延续(由 触发TaskCompletionSource.SetResult)几乎总是异步的,这与 WinForms 或 v4.0 WPF 不同(更多细节)。

4

2 回答 2

9

它在源代码中用很长的注释进行了解释。引用 wpf\src\Base\System\Windows\BaseCompatibilityPreferences.cs 中的 4.5.1 参考源:

    ///     WPF 4.0 had a performance optimization where it would
    ///     frequently reuse the same instance of the
    ///     DispatcherSynchronizationContext when preparing the
    ///     ExecutionContext for invoking a DispatcherOperation.  This
    ///     had observable impacts on behavior.
    ///
    ///     1) Some task-parallel implementations check the reference
    ///         equality of the SynchronizationContext to determine if the
    ///         completion can be inlined - a significant performance win.
    ///
    ///     2) But, the ExecutionContext would flow the
    ///         SynchronizationContext which could result in the same
    ///         instance of the DispatcherSynchronizationContext being the
    ///         current SynchronizationContext on two different threads.
    ///         The continuations would then be inlined, resulting in code
    ///         running on the wrong thread.
    ///
    ///     In 4.5 we changed this behavior to use a new instance of the
    ///     DispatcherSynchronizationContext for every operation, and
    ///     whenever SynchronizationContext.CreateCopy is called - such
    ///     as when the ExecutionContext is being flowed to another thread.
    ///     This has its own observable impacts:
    ///
    ///     1) Some task-parallel implementations check the reference
    ///         equality of the SynchronizationContext to determine if the
    ///         completion can be inlined - since the instances are
    ///         different, this causes them to resort to the slower
    ///         path for potentially cross-thread completions.
    ///
    ///     2) Some task-parallel implementations implement potentially
    ///         cross-thread completions by callling
    ///         SynchronizationContext.Post and Wait() and an event to be
    ///         signaled.  If this was not a true cross-thread completion,
    ///         but rather just two seperate instances of
    ///         DispatcherSynchronizationContext for the same thread, this
    ///         would result in a deadlock.

或者换句话说,他们修复了您代码中的错误 :)

于 2014-03-06T20:38:58.037 回答
5

我相信主要原因是 4.5DispatcherSynchronizationContext还捕获了操作DispatcherPriority,因此它不能被重用(这种行为也可以通过配置BaseCompatibilityPreferences.FlowDispatcherSynchronizationContextPriority)。

关于await- 在异步方法捕获的同步上下文与当前上下文(由返回)SynchronizationContextAwaitTaskContinuation引用相等性中,如果不重用上下文,这当然会失败。因此,操作会在调度程序上排队,而不是内联执行。SynchronizationContext.CurrentNoFlow

这也会影响SynchronizationContextTaskScheduler,它也会执行参照平等检查。

在某些情况下,他们主动选择让异步延续变慢有点令人费解。难道他们不能改变行为以允许比较同步上下文的相等性(例如,通过覆盖Equals和检查它是否属于同一个调度程序)?也许值得打开一个 Connect 问题。

于 2014-03-06T07:56:29.540 回答