为什么每个调度程序都有唯一的同步上下文.开始调用回调?



我刚刚注意到,在.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.ReuseDispatcherSynchronizationContextInstance设置为true将还原 .NET 4.0 行为:

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

研究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); 
} 
}

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

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

它在源代码中用很长的注释进行了解释。 引用自 wpf\src\Base\System\Windows\BaseCompatibility Preferences.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.

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

我相信主要原因是 4.5DispatcherSynchronizationContext还捕获了操作的DispatcherPriority,因此无法重用(此行为也可以通过BaseCompatibilityPreferences.FlowDispatcherSynchronizationContextPriority进行配置)。

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

这也影响SynchronizationContextTaskScheduler,它也执行引用相等性检查。

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

最新更新