共用方式為


SynchronizationCallback

It's common for user interface frameworks (such as Windows Forms applications or Visual Studio packages) to restrict API calls to a single thread; specifically the "UI" thread that is usually the original or "main" thread in the process.  This means that if you spawn a background thread then it can't touch the user interface's objects and must use some other synchronization mechanism to touch the user interface on the main thread instead.  For example in the case of WinForms you can call Invoke or BeginInvoke if you have a reference to a Control.  This usually isn't too much of a burden for most application code since it can easily get access to a Control instance, but it makes it harder for reusable library code to offload things to a background thread in a generic way since it may not have a convenient way to get "back" to the main thread.  It might seem like library code wouldn't need to worry about this since it usually doesn't touch the user interface objects, but the problem comes back because of event handlers.  Often the user interface code will attach event handlers to the reusable library's objects, and if those events are raised from a background thread then the event handler will also be executing on the background thread and will be prone to touching the user interface inappropriately.  This can be guarded against by always having every event handler call Invoke, but that's a huge amount of code overhead just to protect against something that the library might do.

Fortunately, there's a generic class called SynchronizationContext with methods Send and Post (which are analogous to Invoke and BeginInvoke) that is always accessible via the static SynchronizationContext.Current property.  User interface frameworks (like WinForms) override the current SynchronizationContext so that the Send and Post simply call the appropriate Invoke and BeginInvoke methods respectively.  This means that if your library code always uses Send when raising events from its background thread(s) then user interface code will be safe by default.  However, there's a catch: SynchronizationContext.Current is only set in the main thread (which is the only thread where you don't actually need it), as shown with the following code:

 public class MyLibraryClass
{
    public void CalledInMainThread()
    {
        Debug.WriteLine(
            "Main Thread: SynchronizationContext.Current is " +
            (SynchronizationContext.Current != null ?
             SynchronizationContext.Current.ToString() : "null"));

        Thread t = new Thread(new ThreadStart(
                              CalledInBackgroundThread));
        t.Start();
    }

    private void CalledInBackgroundThread()
    {
        Debug.WriteLine(
            "Background Thread: SynchronizationContext.Current is " +
            (SynchronizationContext.Current != null ?
             SynchronizationContext.Current.ToString() : "null"));
    }
}

 

Prints output:

 Main Thread: SynchronizationContext.Current is System.Windows.Forms.WindowsFormsSynchronizationContext

Background Thread: SynchronizationContext.Current is null

The obvious way to work around this is to grab the reference to SynchronizationContext.Current in the main thread and then make it available to the background thread by either storing it in a shared field or by passing it as the object state argument to those callbacks that support it.  The drawback with this method is that the code running in the background needs to keep passing the reference around to everyone who needs it, which kind of defeats the purpose of the static global SynchronizationContext.Current property. 

Fortunately there is a SynchronizationContext.SetSynchronizationContext method that sets the value of the SynchronizationContext.Current property, so if the first method in the background thread always calls SetSynchronizationContext with the instance from the main thread then all of the other code in the background thread can use it without having to pass the reference around.  This also makes it so you can write your code without having to care whether it's going to be called on the main thread or on a background thread since you can assume SynchronizationContext.Current will always be set appropriately.

I use this pattern quite often, so I created a new type of delegate that handles marshalling the SynchronizationContext.Current property from the main thread to the background thread automatically.  I call this delegate type SynchronizationCallback, and it's compatible with all of the other delegate types such as WaitCallback, TimerCallback, ThreadStart, etc.  By simply using this delegate type instead of the original type you'll be able to assume SynchronizationContext.Current will always be valid no matter which thread your code is running on.  For example, by changing the above code as follows:

 public class MyLibraryClass
{
    public void CalledInMainThread()
    {
        Debug.WriteLine(
            "Main Thread: SynchronizationContext.Current is " +
            (SynchronizationContext.Current != null ?
             SynchronizationContext.Current.ToString() : "null"));

        Thread t = new Thread((ThreadStart)new SynchronizationCallback(
                              CalledInBackgroundThread));
        t.Start();
    }

    private void CalledInBackgroundThread(object state)
    {
        Debug.WriteLine(
            "Background Thread: SynchronizationContext.Current is " +
            (SynchronizationContext.Current != null ?
             SynchronizationContext.Current.ToString() : "null"));
    }
}

Then you'll see the output as expected:

 Main Thread: SynchronizationContext.Current is System.Windows.Forms.WindowsFormsSynchronizationContext

Background Thread: SynchronizationContext.Current is System.Windows.Forms.WindowsFormsSynchronizationContext

I've attached the full SynchronizationCallback class for your convenience.  Enjoy!

SynchronizationCallback.cs

Comments

  • Anonymous
    December 02, 2010
    Hi Kael! Just came across your post about SynchronizationContext.  Desktop app project I am working on is still Windows Forms, though we consider migrating to WPF.  Could you comment on how this applies to WPF.  If the discussed approach is used, will it help with WPF in place, or will it create problems?

  • Anonymous
    December 02, 2010
    The best thing about SynchronizationContext is that it works for both WinForms and WPF without any code changes.  In fact, it's what the new async features in .NET 4.5 will use since it's UI-independent.

  • Anonymous
    January 06, 2011
    How do you use this with a BackgroundWorker? Inside the doWork event the SynchronizationContext.Current is empty, even when i pass it as argument of RunWorkerAsync. For the thread example it's very simple and easy to use.

  • Anonymous
    January 06, 2011
    How would you use this code in combination with a BackgroundWorker? From within a Thread it;s simple and saved a lot of references to the main form.

  • Anonymous
    May 05, 2015
    Thank you for this! It's quite useful :)