简体   繁体   English

为什么每个Dispatcher.BeginInvoke回调都有一个唯一的同步上下文?

[英]Why a unique synchronization context for each Dispatcher.BeginInvoke callback?

I've just noticed that with .NET 4.5 each Dispatcher.BeginInvoke / InvokeAsync callback is executed on its own very unique Synchronization Context (an instance of DispatcherSynchronizationContext ). 我刚刚注意到,使用.NET 4.5,每个Dispatcher.BeginInvoke / InvokeAsync回调都在其自己非常独特的同步上下文( DispatcherSynchronizationContext一个实例)上执行。 What's the reason behind this change? 这种变化背后的原因是什么?

The following trivial WPF app illustrates this: 以下简单的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();
        }
    }
}

Output: 输出:

same context #0: False
same context #1: False
same context #2: False
...

Setting BaseCompatibilityPreferences.ReuseDispatcherSynchronizationContextInstance to true restores the .NET 4.0 behavior: BaseCompatibilityPreferences.ReuseDispatcherSynchronizationContextInstance设置为true可恢复.NET 4.0行为:

public partial class App : Application
{
    static App()
    {
        BaseCompatibilityPreferences.ReuseDispatcherSynchronizationContextInstance = true;
    }
}
same context #0: True
same context #1: True
same context #2: True
...

Studying the .NET sources for DispatcherOperation shows this: 研究DispatcherOperation 的.NET源代码显示了这一点:

[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); 
    } 
}

I don't understand why this might be needed, the callbacks queued with Dispatcher.BeginInvoke / InvokeAsync are anyway executed on the correct thread which already has an instance of DispatcherSynchronizationContext installed on it. 我不明白为什么可能需要这个,用Dispatcher.BeginInvoke / InvokeAsync排队的回调无论如何InvokeAsync在已经安装了DispatcherSynchronizationContext实例的正确线程上执行。

One interesting side effect of this change is that await TaskCompletionSource.Task continuation (triggered by TaskCompletionSource.SetResult ) is almost always asynchronous in .NET 4.5 WPF, unlike with WinForms or v4.0 WPF ( some more details ). 这种变化的一个有趣的副作用是await TaskCompletionSource.Task延续(由TaskCompletionSource.SetResult触发)在.NET 4.5 WPF中几乎总是异步的,与WinForms或v4.0 WPF( 更多细节 )不同。

It is explained with a very long comment in the source code. 它在源代码中用很长的注释来解释。 Quoting from the 4.5.1 Reference Source in wpf\\src\\Base\\System\\Windows\\BaseCompatibilityPreferences.cs: 引用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.

Or to put it another way, they fixed the bug in your code :) 或者换句话说,他们修复了代码中的错误:)

I believe the main reason is that the 4.5 DispatcherSynchronizationContext also captures the operation's DispatcherPriority , so it cannot be reused (this behavior is also configurable via BaseCompatibilityPreferences.FlowDispatcherSynchronizationContextPriority ). 我认为主要原因是4.5 DispatcherSynchronizationContext还捕获了操作的DispatcherPriority ,因此无法重用(此行为也可通过BaseCompatibilityPreferences.FlowDispatcherSynchronizationContextPriority配置)。

Regarding await - in SynchronizationContextAwaitTaskContinuation there's a referencial equality for the synchronization context captured by the async method to the current one (returned by SynchronizationContext.CurrentNoFlow ), which of course fails if the context isn't reused. 关于await - 在SynchronizationContextAwaitTaskContinuation ,异步方法捕获的同步上下文与当前的同步上下文(由SynchronizationContext.CurrentNoFlow返回)具有参考相等性,如果不重用上下文,这当然会失败。 So the operation to gets queued on the dispatcher instead of being executed inline. 因此,在调度程序上排队而不是内联执行的操作。

This also affects SynchronizationContextTaskScheduler , which also performs a referencial equality check. 这也会影响SynchronizationContextTaskScheduler ,它也会执行参考等式检查。

Both of these may have been an oversight due to the fact WPF and TPL are developed by different teams. 由于WPF和TPL是由不同的团队开发的,因此这两者都可能是一种疏忽。 Seems like it was done on purpose. 好像它是故意的。 Still, it's a bit puzzling they actively chose to make async continuations slower in some cases. 不过,在某些情况下,他们主动选择让异步延续更慢,这有点令人费解。 Couldn't they change the behavior to allow comparing the sync context's for equality (for example, by overriding Equals and checking it belongs to the same Dispatcher)? 他们不能改变行为以允许比较同步上下文的相等性(例如,通过重写Equals并检查它是否属于同一个Dispatcher)? Maybe it's worth opening a Connect issue. 也许值得打开Connect问题。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM