Section 8.5. Resource Synchronization Context


8.5. Resource Synchronization Context

Incoming service calls execute on worker threads. These threads are managed by WCF and are unrelated to any service or resource threads. This means that by default the service cannot rely on any kind of thread affinity, which is always being accessed by the same thread. Much the same way, the service cannot rely by default on executing on some host-side custom threads created by the host or service developers. The problem with this situation is that some resources may rely on thread affinity, for example user-interfaces resources updated by the service must execute and be accessed only by the user-interface (UI) thread. Another example is a resource (or a service) that makes use of the thread local storage (TLS) to store out-of-band information shared globally by all parties on the same thread. Using the TLS mandates use of the same thread. In addition, for scalability and throughput purposes, some resources or frameworks may require being access by their own pool of threads.

Whenever an affinity to a particular thread or threads is expected, the service cannot simply execute the call on the incoming WCF worker thread. Instead, the service must marshal the call to the correct thread(s) required by the resource it accesses.

8.5.1. .NET 2.0 Synchronization Contexts

.NET 2.0 introduced the concept of a synchronization context. The idea is that any party can provide an execution context and have other parties marshal the calls to that context. The synchronization context can be a single thread or any number of designated threads, although typically it will be just a single, yet particular, thread. All the synchronization context does is assure that the call executes on the correct thread or threads. Note that the word context is overloaded. Synchronization contexts have absolutely nothing to do with the service instance context or the operation context described so far in this book.

While conceptually, synchronization contexts are a simple enough design pattern to use, implementing a synchronization context is a complex programming task that is not intended for developers to normally attempt.

8.5.1.1. The SynchronizationContext class

The class SynchronizationContext from the System.Threading namespace represents a synchronization context:

 public delegate void SendOrPostCallback(object state); public class SynchronizationContext {    public virtual void Post(SendOrPostCallback callback,object state);    public virtual void Send(SendOrPostCallback callback,object state);    public static void SetSynchronizationContext(SynchronizationContext context);    public static SynchronizationContext Current    {get;}    //More members } 

Every thread in .NET 2.0 may have a synchronization context associated with it. You can obtain a thread's synchronization context by accessing the static Current property of SynchronizationContext. If the thread does not have a synchronization context, then Current will return null. You can also pass the reference to the synchronization context between threads, so that one thread can marshal a call to another thread.

To represent the call to invoke in the synchronization context, you wrap a method with a delegate of the type SendOrPostCallback. Note that the signature of the delegate uses an amorphous object. If you want to pass multiple parameters, pack those in a structure and pass the structure as an object.

Synchronization contexts use an amorphous object. Exercise caution when using synchronization contexts due to the lack of compile-time type safety.


8.5.1.2. Working with the synchronization context

There are two ways of marshaling a call to the synchronization context: synchronously and asynchronously, by sending or posting a work item respectively. The Send( ) method will block the caller until the call has completed on the other synchronization context, while Post( ) would merely dispatch it to the synchronization context and then return control to its caller.

For example, to synchronously marshal a call to a particular synchronization context, first you somehow obtain a reference to that synchronization context, and then use the Send( ) method:

 //Obtain synchronization context SynchronizationContext context = ... SendOrPostCallback doWork = delegate(object arg)                             {                                //The code here guaranteed to                                //execute on correct thread(s)                             }; context.Send(doWork,"Some argument"); 

Example 8-4 shows a less abstract example.

Example 8-4. Calling a resource on the correct synchronization context

 class MyResource {    public int DoWork( )    {...}    public SynchronizationContext MySynchronizationContext    {get;} } class MyService : IMyContract {    MyResource GetResource( )    {...}    public void MyMethod( )    {       MyResource resource = GetResource( );       SynchronizationContext context = resource.MySynchronizationContext;       int result = 0;       SendOrPostCallback doWork = delegate                                   {                                      result = resource.DoWork( );                                   };       context.Send(doWork,null);    } } 

In the example, the service MyService needs to interact with the resource MyResource and have it perform some work by executing the DoWork( ) method and returning a result. However, MyResource requires that all calls to it execute on its particular synchronization context. MyResource makes that execution context available via the MySynchronizationContext property. The service operation MyMethod( ) executes on a WCF worker thread. MyMethod( ) first obtains the resource and its synchronization context. MyMethod( ) then defines an anonymous method that wraps the call to DoWork( ), and assigns that anonymous method to the doWork delegate of the type SendOrPostCallback. Finally MyMethod( ) calls Send( ) and passes null for the argument, since the DoWork( ) method on the resource requires no parameters. Note the technique used in Example 8-4 to retrieve a returned value from the invocation. Since Send( ) returns void, the anonymous method assigns the returned value of DoWork( ) into an outer variable. Without anonymous methods, this task would have required the complicated use of a synchronized member variable.

The problem with Example 8-4 is the excessive degree of coupling between the service and the resource. The service needs to know the resource is sensitive to its synchronization context, obtain the context, and manage the execution. It is much better to encapsulate the need in the resource itself, as shown in Example 8-5.

Example 8-5. Encapsulating the synchronization context

 class MyResource {    public int DoWork( )    {       int result = 0;       SendOrPostCallback doWork = delegate                                   {                                     result = DoWorkInternal( );                                   };       MySynchronizationContext.Send(doWork,null);       return result;    }    SynchronizationContext MySynchronizationContext    {get;}    int DoWorkInternal( )    {...} } [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)] class MyService :  IMyContract {    MyResource GetResource( )    (...}    public void MyMethod( )    {       MyResource resource = GetResource( );       int result = resource.DoWork( );    } } 

Compare Example 8-5 to Example 8-4. All the service in Example 8-5 has to do is access the resource. It is up to the service internally to marshal the call to its synchronization context.

8.5.2. UI Synchronization Context

The canonical case for utilizing synchronization contexts is with Windows user interface frameworks such as Windows Forms or the Windows Presentation Foundation (WPF). For simplicity's sake, the rest of the discussion in this chapter will refer only to Windows Forms, although it equally applies to WPF. A Windows UI application relies on the underlying Windows messages and a message-processing loop (the message pump) to process them. The message loop must have thread affinity because messages to a window are delivered only to the thread that created it. In general, you must always marshal to the UI thread any attempt to access a Windows control or a form, or risk errors and failures. This becomes an issue if your services need to update some user interface, as a result of client calls or some other event. Fortunately, Windows Forms support the synchronization context pattern. The thread that pumps messages has a synchronization context. That synchronization context is the WindowsFormsSynchronizationContext class:

 public sealed class WindowsFormsSynchronizationContext : SynchronizationContext,... {...} 

Whenever you call the Application.Run( ) method of Windows Forms to bring up the main window of your application, it not only starts processing windows messages, it also installs WindowsFormsSynchronizationContext as the current thread's synchronization context.

What WindowsFormsSynchronizationContext does is convert the call to Send( ) or Post( ) to a custom Windows message and post that Windows message to the UI thread's message queue. Every Windows Forms UI class that derives from Control has a special method that handles this custom message by invoking the supplied SendOrPostCallback delegate. At some point the custom Windows message is processed by the UI thread and the delegate is invoked.

Because the window or control can also be called already in the correct synchronization context, to avoid a deadlock when calling Send( ), the implementation of the Windows Forms synchronization context verifies that marshaling the call is indeed required. If marshaling is not required, it uses direct invocation on the calling thread.

8.5.2.1. UI access and updates

When a service needs to update some user interface, it must have some proprietary mechanisms to find the window to update in the first place. Once the service has the correct window, it must somehow get hold of that window's synchronization context and marshal the call to it. Such a possible interaction is shown in Example 8-6.

Example 8-6. Using the form synchronization context

 partial class MyForm : Form {    Label m_CounterLabel;    SynchronizationContext m_SynchronizationContext;    public MyForm( )    {       InitializeComponent( );       m_SynchronizationContext = SynchronizationContext.Current;       Debug.Assert(m_SynchronizationContext != null);    }    public SynchronizationContext MySynchronizationContext    {       get       {          return m_SynchronizationContext;       }    }    public int Counter    {       get       {          return Convert.ToInt32(m_CounterLabel.Text);       }       set       {          m_CounterLabel.Text = value.ToString( );       }    } } [ServiceContract] interface IFormManager {    [OperationContract]    void IncrementLabel( ); } [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)] class MyService : IFormManager {    public void IncrementLabel( )    {       MyForm form = Application.OpenForms[0] as MyForm;       Debug.Assert(form != null);       SendOrPostCallback callback = delegate                                     {                                        form.Counter++;                                     };       form.MySynchronizationContext.Send(callback,null);    } } static class Program {    static void Main( )    {       ServiceHost host = new ServiceHost(typeof(MyService));       host.Open( );       Application.Run(new MyForm( ));       host.Close( );    } } 

Example 8-6 shows the form MyForm that provides the property MySynchronizationContext, allowing its clients to obtain its synchronization context. MyForm initializes MySynchronizationContext in its constructor by obtaining the synchronization context of the current thread. The thread has a synchronization context because the Main( ) method called Application.Run( ), which triggered the message pump. MyForm also offers the Counter property that updates the value of a counting Windows Forms label. Counter must be accessed by the thread that owns the form. The service MyService implements the IncrementLabel( ) operation. In that operation, the service obtains a reference to the form via the static OpenForms collection of the Application class:

 public class FormCollection : ReadOnlyCollectionBase {    public virtual Form this[int index]    {get;}    public virtual Form this[string name]    {get;} } public sealed class Application {    public static FormCollection OpenForms    {get;}    //Rest of the members } 

Once IncrementLabel( ) has the form to update, it accesses the synchronization context via the MySynchronizationContext, and calls the Send( ) method. Send( ) is provided with an anonymous method that updates Counter. Example 8-6 is a concrete example of the programming model shown in Example 8-4. Much the same way, the technique of Example 8-6 suffers from the same deficiency; namely, tight coupling between the service and the form. If the service needs to update multiple controls, that also results in a cumbersome programming model. Any change to the user interface layout, the controls on the forms, and the required behavior is likely to cause major changes to the service code.

8.5.2.2. Safe controls

It is better to encapsulate the interaction with the Windows Forms synchronization context in safe controls or safe methods on the form to decouple them from the service and to simplify the overall programming model. Example 8-7 lists the code for SafeLabel, a Label-derived class that provides a thread-safe access to its Text property. Because SafeLabel derives from Label, you still have all the design-time visual experience and integration with Visual Studio, yet you surgically affect just the property that requires the safe access.

Example 8-7. Encapsulating the synchronization context

 public class SafeLabel : Label {    SynchronizationContext m_SynchronizationContext =                                           SynchronizationContext.Current;    override public string Text    {       set       {          SendOrPostCallback setText = delegate(object text)                                       {                                          base.Text = text as string;                                       };          m_SynchronizationContext.Send(setText,value);       }       get       {          string text = String.Empty;          SendOrPostCallback getText = delegate                                       {                                          text = base.Text;                                       };          m_SynchronizationContext.Send(getText,null);          return text;       }    } } 

Upon construction, SafeLabel caches its synchronization context. SafeLabel overrides its base class Text property, and uses an anonymous method in the get and set accessors to send the call to the correct UI thread. Note in the get accessor the use of an outer variable to return a value from Send( ) as discussed previously. Using SafeLabel, the code in Example 8-6 is reduced to the code shown in Example 8-8.

Example 8-8. Using a safe control

 class MyForm : Form {    Label m_CounterLabel;    public MyForm( )    {       InitializeComponent( );    }    void InitializeComponent( )    {       ...       m_CounterLabel = new SafeLabel( );       ...    }    public int Counter    {       get       {          return Convert.ToInt32(m_CounterLabel.Text);       }       set       {          m_CounterLabel.Text = value.ToString( );       }    } } [ServiceBehavior(InstanceContextMode = InstanceContextMode.PerCall)] class MyService : IFormManager {    public void IncrementLabel( )    {       MyForm form = Application.OpenForms[0] as MyForm;       Debug.Assert(form != null);       form.Counter++;    } } 

Note in Example 8-8 that the service simply accesses the form directly:

 form.Counter++; 

and that the form is written as a normal form. Example 8-8 is a concrete example of the programming model shown in Example 8-5.

The source code accompanying this book contains, in the assembly ServiceModelEx.dll, the code not only for SafeLabel but also for other commonly used controls, such as SafeButton, SafeListBox, SafeProgressBar, SafeStatusBar, and SafeTextBox.





Programming WCF Services
Programming WCF Services
ISBN: 0596526997
EAN: 2147483647
Year: 2004
Pages: 148
Authors: Juval Lowy

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net