SynchronizationContext and program flow

Many of you seen code like this:

public async Task Func()
{
    await SomeFuncAsync().ConfigureAwait(true);
    SomeOtherFunc();
}

Let’s assume that this code is executing in main application thread. Is it possible that SomeOtherFunc will be executed not in main application thread? Answer is yes. Let me explain why.

Here is really really simplified version of what compiler will do with our function:

public Task Func()
{
    Task task = SomeFuncAsync();
    SynchronizationContext context = SynchronizationContext.Current;
    task.ContinueWith(finishedTask => context.Post(state => { SomeOtherFunc(); }, null));
    task.Start();
    return task;
}

It actually looks completely different but from point of SynchronizationContext it looks close enough.

In short, when task will finish, it will execute provided callback in the same thread that just finished execution of task. Callback calls context.Post and providing another callback that will actually execute SomeOtherFunc. SynchronizationContext.Post should switch context back to thread that called our Func.

This is how it works in theory. But there are many catches. By default, threads have only default SynchronizationContext. That context is very simple and here all it does:

public virtual void Post(SendOrPostCallback d, Object state)
{
    ThreadPool.QueueUserWorkItem(new WaitCallback(d), state);
}

If you have some background thread and will try to call Func there is good chance that SomeOtherFunc will be executed in different background thread. But there is small change that it will be executed in current thread. If you have Console application, then there is also only default SynchronizationContext and SomeOtherFunc will be executed in some background thread. If you need Func to work correctly in case of console application or background thread, then you have to create and use different SynchronizationContext or implement your own. Search internet for details.

Now let’s talk about WPF and WinForms applications. If you call Func from main application thread of WPF or WinForms and check System.Threading.SynchronizationContext.Current, you will see that it is not default one and SomeOtherFunc will be executed from main application thread. In case WPF you will see System.Windows.Threading.DispatcherSynchronizationContext; WinForms has System.Windows.Forms.WindowsFormsSynchronizationContext. And if you continue to run Func under debugger you will see SomeOtherFunc executed in main application thread. Looks like everything works correctly. Well most of the time it does. But there are cases when it does not work correctly.

Most people believe that SynchronizationContext is set by WPF or WinForms initialization or something like that and will be there until application finishes. But it is incorrect. Let me show how context is set using WPF application as example.

Put breakpoint to System.Threading.ExecutionContext.set_SynchronizationContext and run WPF application you will see following callstack:

System.Threading.ExecutionContext.set_SynchronizationContext (source line information unavailable)
System.Threading.SynchronizationContext.SetSynchronizationContext (source line information unavailable)
System.Windows.Threading.Dispatcher.LegacyInvokeImpl (source line information unavailable)
System.Windows.Threading.Dispatcher.Invoke (source line information unavailable)
MS.Win32.HwndSubclass.SubclassWndProc (source line information unavailable)

Then you can check .NET source code you will see something like this:

            SynchronizationContext oldSynchronizationContext = SynchronizationContext.Current;
            try
            {
                DispatcherSynchronizationContext newSynchronizationContext;
                if (BaseCompatibilityPreferences.GetReuseDispatcherSynchronizationContextInstance())
                {
                    newSynchronizationContext = _defaultDispatcherSynchronizationContext;
                }
                else
                {
                    if (BaseCompatibilityPreferences.GetFlowDispatcherSynchronizationContextPriority())
                    {
                        newSynchronizationContext = new DispatcherSynchronizationContext(this, priority);
                    }
                    else
                    {
                        newSynchronizationContext = new DispatcherSynchronizationContext(this, DispatcherPriority.Normal);
                    }
                }
                SynchronizationContext.SetSynchronizationContext(newSynchronizationContext);
                return WrappedInvoke(method, args, numArgs, null);
            }
            finally
            {
                SynchronizationContext.SetSynchronizationContext(oldSynchronizationContext);
            }

This will tell us that synchronization context is set in windows procedure and after message processed, it will reset back to original default context. And I checked value of oldSynchronizationContext and it is default one. Usually Microsoft does quite good job and every user interaction are covered (timers and similar things are covered too), and context will be set but there are few corner cases that are not covered. One example when .NET calls from externally. For example, from WinApi callback. Another example is our application that is using COM interfaces to communicate between managed and native code. And when some .NET object implements interface and it called from native code, then there will be default SynchronizationContext and our Func will not work as intended.

Ok, now we understand problem, but how to fix it? You can call Func like that:

Dispatcher.CurrentDispatcher.Invoke(()=>Func())

And it will work correctly but unfortunately you have to know where you code is calling from. Another solution would be to check if SynchronizationContext.Current is System.Windows.Threading.DispatcherSynchronizationContext and if not, use Dispatcher.CurrentDispatcher.Invoke

I this will be helpful to someone.

Comments

Post comment