Comparing SynchronizationContext
Asked Answered
L

1

10

How do I compare SynchronizationContext? It seems that the same Dispatcher can create different SynchronizationContext when using BeginInvoke. When I drill down into the two (unequal) contexts, I see that the dispatcher Thread ID is the same, yet they are not Equal to each other.

public partial class MainWindow : Window
{
    private SynchronizationContext contexta;
    private SynchronizationContext contextb;
    private SynchronizationContext contextc;
    private SynchronizationContext contextd;

    public MainWindow()
    {
        InitializeComponent();

        contexta = SynchronizationContext.Current;

        Loaded += MainWindow_Loaded;
    }

    private void MainWindow_Loaded(object sender, RoutedEventArgs e)
    {
        contextb = SynchronizationContext.Current;

        Dispatcher.Invoke(() =>
            {
                contextc = SynchronizationContext.Current;
            });

        Dispatcher.BeginInvoke(new Action(() =>
            {
                contextd = SynchronizationContext.Current;
            }));

        Debug.Assert(contexta != contextb);
        Debug.Assert(contexta == contextc);         // fails... why?!?!?
        Debug.Assert(contexta == contextd);         // fails... why?!?!?
        Debug.Assert(contextc == contextd);         // fails... why?!?!?
    }        

Maybe the two of them cannot be used together. I noticed that this actually works:

        contexta.Send(new SendOrPostCallback((s) =>
            {
                contexte = SynchronizationContext.Current;
            }), null);

Update But strangely, it doesn't always work.

    public override void AddRange(IEnumerable<T> items)
    {
        if (SynchronizationContext.Current == _context)
        {
            base.AddRange(items);
        }
        else
        {
            _context.Send(new SendOrPostCallback((state) =>
                {
                    AddRange(state as IEnumerable<T>);
                }), items);
        }
    }

never gets a matched _context and goes on forever, for example. Even though it shouldn't. This latter example the threads actually end up being the same, and there is a context, but it is different.

Update2 Ok, I got it to work, but I really feel uncomfortable about it. Apparently, when you Post or Send, your task is run from the right thread, but if you aren't coming from the UI, it seems that a new SynchronizationContext is generated.

    public override void AddRange(IEnumerable<T> items)
    {
        if (SynchronizationContext.Current == _context)
        {                       
            base.AddRange(items);
        }
        else
        {
            _context.Post(new SendOrPostCallback((state) =>
                {
                    if (SynchronizationContext.Current != _context)
                        SynchronizationContext.SetSynchronizationContext(_context);     // called every time.. strange
                    AddRange(items);
                }), null);
        }
    }

And look at this:

enter image description here

"Requires full trust for the immediate caller. This member cannot be used by partially trusted or transparent code." :(

Lubeck answered 21/11, 2012 at 18:46 Comment(2)
Once you post from _context, it is actually executing in the correct thread. So it is conceivable that SetSync is not really needed.Lubeck
Has this just changed with .Net 4.5? I have an event aggregator that relied on getting the same synchronization context so that I could just invoke event actions that were already being published on the correct(subscriber) thread. It broke when I moved from 4 to 4.5.Hawes
R
2

I think you are interested in BaseCompatibilityPreferences.ReuseDispatcherSynchronizationContextInstance.

This setting dictates whether a single SynchronizationContext instance is used for a given Dispatcher object or not. It is true by default until .net 4, and false in .net 4.5 (this is the behavior change that LukeN observes).

Now if your goal is just to make a direct call rather than calling .Send() I'd say:

  1. when calling .Send(), the DispatcherSynchronizationContext actually just makes a direct call if you are on the correct thread (doesn't use the dispatcher Queue) so you're not gaining much anyway (a few checks and calls from the extra layers of indirection).

  2. if you only code against WPF, you can use Dispatcher.CheckAccess() and Dispatcher.Invoke() to do what you want.

In the general case, there is no way to "compare" two SynchronizationContexts, so you should just call .Send(). It's not likely to be a performance issue anyway, remember that premature optimization is the root of all evil -> measure first.

Riesling answered 29/5, 2013 at 23:51 Comment(5)
Interestingly, SynchronizationContextTaskScheduler.TryExecuteTaskInline uses reference equality to compare synchronization contexts, so this behavior change is presumably also affecting the TPL.Joli
Some legacy Windows Forms code I am using uses a "GenericSynchronizingObject" in some UI methods to know whether to call a method directly or call Post() for asynchronous execution. I recently upgraded to .net 4.5.2, and seeing the behavior that @Hawes described. Does that mean I can just forego the check for (synchronizationContext != SynchronizationContext.Current (which are now never equal after upgrading) and Post() will call directly if the context is the same?Zeal
I wanted to force BaseCompatibilityPreferences.ReuseDispatcherSynchronizationContextInstance to true just to maintain the current behavior. However, I can only seem to select from the .net 4.0 version of the dll, and contrary to what it says in the answer above, BaseCompatibilityPreferences.ReuseDispatcherSynchronizationContextInstance only seems to be available in the .net 4.5 version of the dll (I get an undefined error, confirmed by the docs - msdn.microsoft.com/en-us/library/…)Zeal
@Zeal my answer is poorly worded. Until 4.0 ReuseDispatcherSynchronizationContextInstance did not exist but the framework behaves as if it was true. It was introduced in 4.5, whereas if your app explicitly targets 4.0 it defaults to true, otherwise it's false. If your app targets 4.5 you can set it to true to opt in the old behaviour, yes. Otherwise you can explicitely target 4.0.Riesling
Also, if you use Windows Forms, you can use InvokeRequired to detect whether you need to call Invoke() or not (i.e. whether you're on the same thread where the control was created or not).Riesling

© 2022 - 2024 — McMap. All rights reserved.