Почему уникальный контекст синхронизации для каждого обратного вызова 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 < 10 )
{
i++;
test();
}
});
};
this.Loaded += (s, e) => test();
}
}
}
Вывод:
same context #0: False
same context #1: False
same context #2: False
...
Настройка 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
...
Изучение источники .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
, установленный на нем.
Один интересный побочный эффект этого изменения заключается в том, что продолжение await TaskCompletionSource.Task
(вызванное TaskCompletionSource.SetResult
) почти всегда асинхронно в .NET 4.5 WPF, в отличие от WinForms или v4.0 WPF (несколько подробнее).
Ответы
Ответ 1
Это объясняется очень длинным комментарием в исходном коде. Цитирование из 4.5.1 Исходного источника в 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.
Или, говоря иначе, они исправили ошибку в вашем коде:)
Ответ 2
Я считаю, что основная причина заключается в том, что 4.5 DispatcherSynchronizationContext
также фиксирует операцию DispatcherPriority
, поэтому ее нельзя использовать повторно (это поведение также можно настроить с помощью BaseCompatibilityPreferences.FlowDispatcherSynchronizationContextPriority
).
Относительно await
- in SynchronizationContextAwaitTaskContinuation
существует референциальное равенство для контекста синхронизации, захваченного методом async, к текущему (возвращается SynchronizationContext.CurrentNoFlow
), который, конечно, терпит неудачу, если контекст не используется повторно. Таким образом, операция будет поставлена в очередь на диспетчере вместо выполнения встроенного.
Это также влияет на SynchronizationContextTaskScheduler
, который также выполняет проверку референциального равенства.
Оба из них, возможно, были надзором из-за того, что WPF и TPL разрабатываются разными командами. Похоже, что это было сделано специально. Тем не менее, это немного озадачило, что они активно выбирали сделать асинхронные продолжения более медленными в некоторых случаях. Не могли ли они изменить поведение, чтобы позволить сравнивать контекст синхронизации для равенства (например, переопределяя Equals
и проверяя его принадлежность к тому же Диспетчеру)? Возможно, стоит открыть проблему с Connect.