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
). What's the reason behind this change?
The following trivial WPF app illustrates this:
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:
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:
[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.
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).
SynchronizationContext is a representation of the current environment that our code is running in. That is, in an asynchronous program, when we delegate a unit of work to another thread, we capture the current environment and store it in an instance of SynchronizationContext and place it on Task object.
NET Framework has its own SynchronizationContext , in contrast ASP.NET Core does not. That means that code running in an ASP.NET Core app by default won't see a custom SynchronizationContext , which lessens the need for ConfigureAwait(false) running in such an environment.
CurrentDispatcher may create a new instance of Dispatcher depending on the thread from which it was called. That is correct, the Application. Current. Dispatcher is an instance property of the application which is assigned upon construction to be the dispatcher of the current thread.
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 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
).
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. 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.
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)? Maybe it's worth opening a Connect issue.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With