Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
289 views
in Technique[技术] by (71.8m points)

c# - 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). 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).

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

It is explained with a very long comment in the source code. Quoting from the 4.5.1 Reference Source in wpfsrcBaseSystemWindowsBaseCompatibilityPreferences.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 :)


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...