The .NET Framework's New SynchronizationContext Class

Introduction

The SynchronizationContext class is a new class belonging to the .NET Framework's System.Threading namespace. The purpose of this class is to provide a model to make communication between threads easier and more robust. I will begin by describing how the SynchronizationContext class helps us handle events using Windows Forms. I will also touch on a few of the other new classes in the System.ComponentModel which use the SynchronizationContext class that also help us with synchronization issues.

As a disclaimer, some of what I describe below is based on my experience, albeit brief, with these new classes rather than documentation. The documentation seems a bit sparse at this time. So while my understanding isn't complete, I'm hoping that what I share below will be helpful, and that I will be able to expand this article in the future as my understanding (and hopefully Microsoft's documentation) increases.

Background

Most of us are familiar with the prohibition against modifying a Control from any thread other than the one in which it was created. Instead, an event generated on another thread must be marshaled to the Control's thread using its (or the Form that it belongs to) Invoke or BeginInvoke methods. These two methods belong to the ISynchronizeInvoke interface, which the Control class implements. Their purpose is to take a delegate and invoke it on the same thread that the ISynchronizeInvoke object is running. Typically, a Form's method for responding to an event generated on another thread looks like this:

Collapse
private void HandleSomeEvent(object sender, EventArgs e)
{
if(InvokeRequired)
{
BeginInvoke(new EventHandler(HandleSomeEvent), sender, e);
}
else
{
// Event logic here.

}
}

This method first checks its InvokeRequired boolean property, which also belongs to the ISynchronizeInvoke interface, to see if it needs to marshal the event to its thread. If the property is true, it calls the BeginInvoke method, passing it a delegate to the method for handling the event and its arguments. If the property is false, it executes its logic for responding to the event. In other words, the InvokeRequired property will be true if it is checked on a thread other than the one in which the Form belongs; otherwise, it will be false.

Note that the delegate being passed to BeginInvoke represents the same method that handled the event in the first place. If the InvokeRequired property is true, this has the effect of invoking the event handler method twice: once when it initially is called in response to the event, and once after it has been marshaled by the Form with a call to BeginInvoke. The second time around, the InvokeRequired property will be false and the event logic will be run.

The SynchronizationContext Class to the Rescue!

Wouldn't it be nice if Forms didn't have to check to see if an event was raised on another thread? Wouldn't it be nice if a Form could respond to an event without having to check its InvokeRequired property? The SynchronizationContext class solves our problem.

The SynchronizationContext class represents a conduit through which we can pass delegates to be invoked on the same thread that the SynchronizationContext represents. It is like the ISynchronizeInvoke interface in that respect. Corresponding to the ISynchronizeInvoke's Invoke and BeginInvoke methods, the SynchronizationContext class has Send and Post methods. Like the Invoke method, the SynchronizationContext's Send method is for invoking delegates synchronously, and like the BeginInvoke method, the Post method is for invoking delegates asynchronously.

Both the Send and Post methods take a SendOrPostCallback delegate representing the method to be invoked. In addition, they also take an object representing state information to pass to the delegate when it is invoked.

At this point, we may be wondering what advantage the SynchronizationContext class gives us. Why not use the ISynchrnoizeInvoke interface instead?

Well, this is where things get interesting. The SynchronizationContext class has a SetSynchronizationContext static method for setting the current SynchronizationContext object. What is going on behind the scenes is that the SetSynchronizationContext static method takes the SynchronizationContext object passed to it and associates it with the current thread, the thread in which the SetSynchronizationContext method is being called. Afterwards, the SynchronizationContext object can be retrieved using the Current static property. This property represents the SynchronizationContext object for the current thread, whichever thread you happen to be in when you access the Current property.

Let's go over this one more time:

When the SetSynchronizationContext static method is called, it takes the SynchronizationContext object passed to it, and keeps track of it and the thread it belongs to. It makes sure that when you access the Current static property, you get back whichever SynchronizationContext object was set for that thread.

So getting back to our Form scenario described above, when a Form is created, a SynchronizationContext object (actually a class object derived from SynchronizationContext, called WindowsFormsSynchronizationContext, more on that later) representing the Form's thread is set. Other objects can retrieve the Form's SynchronizationContext object through the Current property and use it later for marshaling delegate invocations to the Form's thread. In this way, Forms no longer have to check to see if an event they are responding to originated on another thread. The marshaling has already been taken care of elsewhere. In essence, the responsibility for marshaling events has been moved from the receiver of an event to the sender.

This model helps us automate communication between threads. A thread can communicate with another thread safely by using its SynchronizationContext object.

There's something that I now need to tell you: the SynchronizationContext class itself doesn't do very much on its own. While it is not an abstract class, it is really meant to be a base class for classes that override its methods to provide meaningful functionality. For example, here is its implementation for the Send and Post methods:

Collapse
public virtual void Send(SendOrPostCallback d, Object state)
{
d(state);
}

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

The Send method simply invokes the callback delegate, passing it the state argument. The Post method provides asynchronous behavior by queuing the callback as a work item with the ThreadPool. So the default behavior of the SynchronizationContext class doesn't really do a whole lot. The magic comes when we use or create a derived class that does something more meaningful.

An example of a SynchronizationContext derived class is the WindowsFormsSynchronizationContext class. Forms use this class to represent their synchronization context. My guess is that this class is a light wrapper around a Form's ISynchronizeInvoke functionality, delegating calls to Send and Post to its Invoke and BeginInvoke methods, respectively.

Using the SynchronizationContext Class

How do we write our own classes that use the SynchronizationContext class? The key thing to remember is that our goal is to get the SynchronizationContext belonging to the thread in which our class was created so that we can use it later from another thread to send/post events to the original thread. I'm going to give a really simple example. This example does nothing useful, but almost at a glance, it will show us the basic template for using the SynchronizationContext class:

Collapse
using System;
using System.Threading;

namespace SynchronizationContextExample
{
public class MySynchronizedClass
{
private Thread workerThread;

private SynchronizationContext context;

public event EventHandler SomethingHappened;

public MySynchronizedClass()
{
// It's important to get the current SynchronizationContext

// object here in the constructor. We want the

// SynchronizationContext object belonging to the thread in

// which this object is being created.

context = SynchronizationContext.Current;

// It's possible that the current thread does not have a

// SynchronizationContext object; a SynchronizationContext

// object has not been set for this thread.

//

// If so, we simplify things by creating a SynchronizationContext

// object ourselves. However! There could be some problems with

// this approach. See the article for more details.

if(context == null)
{
context = new SynchronizationContext();
}

workerThread = new Thread(new ThreadStart(DoWork));

workerThread.Start();
}

private void DoWork()
{
context.Post(new SendOrPostCallback(delegate(object state)
{
EventHandler handler = SomethingHappened;

if(handler != null)
{
handler(this, EventArgs.Empty);
}
}), null);
}
}
}

This class gets the SynchronizationContext for the current thread in its constructor. If this class is being used in a Form, the current SynchronizationContext object will allow us to post/send events to the Form's thread. It's possible, however, that if an instance of this class is being created in, say, a worker thread somewhere, the SynchronizationContext.Current property may be null. In other words, the SynchronizationContext object for the current thread may not have been set. So, it's important to check to see if the Current property is null. Here, in the case where it is null, I just create an instance of the SynchronizationContext class and rely on its default behavior.

What's nice is that our class doesn't have to know about who it's sending/posting events to. It doesn't have to have an ISynchronizeInvoke object passed to it to synchronize itself with; it has access to the current SynchronizationContext object through the SynchronizationContext.Current property.

Warning! As was pointed out in a post to this article's message board, creating an instance of the SynchronizationContext class can be dangerous. For example, say that you're writing a Form based application and an object needs to interact with the main Form in a thread safe way; the object needs access to the SynchronizationContext that belongs to the Form's thread. The object can retrieve this by accessing the SynchronizationContext.Current property. This will give you the Form's SynchronizationContext derived object assuming that the Current property is checked on the same thread in which the Form is running. If it's checked from another thread, the Current property will probably be null. If this is the case, it would be safer to treat this as an error rather than simply create an instance of the SynchronizationContext class.

The AsyncOperation and AsyncOperationManager Classes

The AsyncOperation and AsyncOperationManager classes make our life even easier by removing the need for accessing the SynchronizationContext object directly. Here is the above example rewritten using both of these classes:

Collapse
using System;
using System.Threading;
using System.ComponentModel;

namespace SynchronizationContextExample
{
public class MySynchronizedClass
{
private Thread workerThread;

private AsyncOperation operation;

public event EventHandler SomethingHappened;

public MySynchronizedClass()
{
operation = AsyncOperationManager.CreateOperation(null);

workerThread = new Thread(new ThreadStart(DoWork));

workerThread.Start();
}

private void DoWork()
{
operation.Post(new SendOrPostCallback(delegate(object state)
{
EventHandler handler = SomethingHappened;

if(handler != null)
{
handler(this, EventArgs.Empty);
}
}), null);

operation.OperationCompleted();
}
}
}

It looks as though the AsyncOperationManager takes care of checking if the current SynchronizationContext object is null and providing us with one (wrapped inside an AsyncOperation object) if that's the case.

The BackgroundWorker Class

An example of a class that uses this new model is the BackgroundWorker class. This class lets you run an operation in the background as you do other work. It provides functionality for raising events to let you know how the operation is progressing as well as when it has completed. When using a BackgroundWorker with a Form, the Form does not have to check to see if the BackgroundWorker's events are being raised from a different thread. The BackgroundWorker class takes care of that for us by using the Form's SynchronizationContext object.

Conclusion

There are a few other methods in the SynchronizationContext class that I haven't covered. To be honest, I'm not sure I understand their purpose. Hopefully, in time, I'll be able to add more details to this article that will provide greater understanding of this new and useful class. In fact, I encourage you to respond with comments and suggestions to make this article more informative. I'm really excited about these new classes as I believe that Microsoft is continuing to make advances in the way that we program.

I hope that you have enjoyed the article, and I look forward to hearing from you. Thanks.

History

  • May 26, 2006 - First version completed.
  • May 7, 2006 - Updated with a warning about using a raw SynchronizationContext object.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here

About the Author

Leslie Sanford


Member
Aside from dabbling in BASIC on his old Atari 1040ST years ago, Leslie's programming experience didn't really begin until he discovered the Internet in the late 90s. There he found a treasure trove of information about two of his favorite interests: MIDI and sound synthesis.

After spending a good deal of time calculating formulas he found on the Internet for creating new sounds by hand, he decided that an easier way would be to program the computer to do the work for him. This led him to learn C. He discovered that beyond using programming as a tool for synthesizing sound, he loved programming in and of itself.

Eventually he taught himself C++ and C#, and along the way he immersed himself in the ideas of object oriented programming. Like many of us, he gotten bitten by the design patterns bug and a copy of GOF is never far from his hands.

Now his primary interest is in creating a complete MIDI toolkit using the C# language. He hopes to create something that will become an indispensable tool for those wanting to write MIDI applications for the .NET framework.

Besides programming, his other interests are photography and playing his Les Paul guitars.
Location: United States United States

你可能感兴趣的:(framework)