为什么每个Dispatcher.BeginInvoke回调都有一个唯一的同步上下文?
我刚刚注意到,使用.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 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
排队的回调无论如何InvokeAsync
在已经安装了DispatcherSynchronizationContext
实例的正确线程上执行。
这种变化的一个有趣的副作用是await TaskCompletionSource.Task
延续(由TaskCompletionSource.SetResult
触发)在.NET 4.5 WPF中几乎总是异步的,与WinForms或v4.0 WPF( 更多细节 )不同。
它在源代码中用很长的注释来解释。 引用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.
或者换句话说,他们修复了代码中的错误:)
我认为主要原因是4.5 DispatcherSynchronizationContext
还捕获了操作的DispatcherPriority
,因此无法重用(此行为也可通过BaseCompatibilityPreferences.FlowDispatcherSynchronizationContextPriority
配置)。
关于await
– 在SynchronizationContextAwaitTaskContinuation
,异步方法捕获的同步上下文与当前的同步上下文(由SynchronizationContext.CurrentNoFlow
返回)具有参考相等性,如果不重用上下文,这当然会失败。 因此,在调度程序上排队而不是内联执行的操作。
这也会影响SynchronizationContextTaskScheduler
,它也会执行参考等式检查。
由于WPF和TPL是由不同的团队开发的,因此这两者都可能是一种疏忽。 好像它是故意的。 不过,在某些情况下,他们主动选择让异步延迟更慢,这有点令人费解。 他们不能改变行为以允许比较同步上下文的相等性(例如,通过重写Equals
并检查它是否属于同一个Dispatcher)? 也许值得打开Connect问题。