Section 8.9. Callbacks and Synchronization Context


8.9. Callbacks and Synchronization Context

Similar to a service invocation, the callback may need to access resources that rely on some kind of thread(s) affinity. In addition, the callback instance itself may require thread affinity for its own use of the TLS or for interacting with a UI thread. While the callback can use techniques such as those in Examples 8-4 and 8-5 to marshal the interaction to the resource synchronization context, you can also have WCF associate the callback with a particular synchronization context by setting the UseSynchronizationContext property to true. However, unlike the service, the client does not use any host to expose the endpoint. If the UseSynchronizationContext property is true, the synchronization context to use is locked in when the proxy is opened, or, more commonly, when the client makes the first call to the service using the proxy, if Open( ) is not explicitly called. If the calling client thread has a synchronization context, this will be the synchronization context used by WCF for all callbacks to the client's endpoint associated with that proxy. Note that only the first call (or the call to Open( )) made on the proxy is given the opportunity to determine the synchronization context. Subsequent calls have no say in the matter. If the calling client thread has no synchronization context, even if UseSynchronizationContext is true, no synchronization context will be used for the callbacks.

8.9.1. Callbacks and UI Synchronization Context

If the callback object is running in a Windows Forms synchronization context, or if it needs to update some UI, you must marshal the callbacks or the updates to the UI thread. You can use techniques such as those in Examples 8-6 or 8-8. However, the more common use for UI updates over callbacks is to have the form itself implement the callback contract and update the UI as in Example 8-20.

Example 8-20. Relying on the UI synchronization context for callbacks

 partial class MyForm : Form,IMyContractCallback {    MyContractClient m_Proxy;    public MyForm( )    {       InitializeComponent( );       InstanceContext callbackContext = new InstanceContext(this);       m_Proxy = new MyContractClient(callbackContext);    }    //Called as a result of a UI event    public void OnCallService(object sender,EventArgs args)    {       m_Proxy.DoSomething( );//Affinity established here    }    //This method always runs on the UI thread    public void OnCallback( )    {       //No need for synchronization and marshaling       Text = "Some Callback";    }    public void OnClose(object sender,EventArgs args)    {       m_Proxy.Close( );    } } 

In Example 8-20 the proxy is first used in the CallService( ) method, which is called by the UI thread as a result of some UI event. Calling the proxy on the UI synchronization context establishes the affinity to it, so the callback can directly access and update the UI without marshaling any calls. In addition, since only one thread (and the same thread at that) will ever execute in the synchronization context, the callback is guaranteed to be synchronized.

You can also explicitly establish the affinity to the UI synchronization context by opening the proxy in the form's constructor without invoking an operation. This is especially useful if you want to dispatch calls to the service on worker threads (or perhaps even asynchronously as discussed at the end of this chapter), and yet have the callbacks enter on the UI synchronization context, as shown in Example 8-21.

Example 8-21. Explicitly opening a proxy to establish synchronization context

 partial class MyForm : Form,IMyContractCallback {    MyContractClient m_Proxy;    public MyForm( )    {       InitializeComponent( );       InstanceContext callbackContext = new InstanceContext(this);       m_Proxy = new MyContractClient(callbackContext);       //Establish affinity to UI synchronization context here:       m_Proxy.Open( );    }    //Called as a result of a UI event    public void CallService(object sender,EventArgs args  )    {       ThreadStart invoke = delegate                            {                               m_Proxy.DoSomething( );                            };       Thread thread = new Thread(invoke);       thread.Start( );    }    //This method always runs on the UI thread    public void OnCallback( )    {       //No need for synchronization and marshaling       Text = "Some Callback";    }    public void OnClose(object sender,EventArgs args)    {       m_Proxy.Close( );    } } 

8.9.1.1. UI thread callbacks and responsiveness

When callbacks are processed on the UI thread, the UI itself is not responsive. Even if you perform relatively short callbacks, if the callback is configured with ConcurrencyMode.Multiple, there could be multiple callbacks back-to-back in the UI message queue, and processing them all at once will degrade responsiveness. You should avoid lengthy callback processing on the UI thread and opt for configuring the callback with ConcurrencyMode.Single so that the callback object lock will queue up the multiple callbacks, and by dispatching them one at a time to the callback object you enable interleaving them among the UI messages.

8.9.1.2. UI thread callbacks and concurrency management

Configuring the callback for affinity to the UI thread may trigger a deadlock. Consider the following setup: A Windows Forms client establishes affinity between a callback object (or even itself) and the UI synchronization context. The client then calls a service passing the callback reference. The service is configured for reentrancy and it calls the client. A deadlock occurs now because the callback to the client needs to execute on the UI thread, and that thread is blocked waiting for the service call to return. Configuring the callback as a one-way operation will not resolve the problem here because the one-way call still needs to be marshaled first to the UI thread. The only way to resolve the deadlock in this case is to turn off using the UI synchronization context by the callback, and to manually and asynchronously marshal the update to the form using its synchronization context. Example 8-22 demonstrates using this technique.

Example 8-22. Avoiding callback deadlock on the UI thread

 ////////////////////////// Client Side ///////////////////// [CallbackBehavior(UseSynchronizationContext = false)] partial class MyForm : Form,IMyContractCallback {    SynchronizationContext m_Context;    MyContractClient m_Proxy;    public MyForm( )    {       InitializeComponent( );       m_Context = SynchronizationContext.Current;       InstanceContext callbackContext = new InstanceContext(this);       m_Proxy = new MyContractClient(callbackContext);    }    public void CallService(object sender,EventArgs args)    {       m_Proxy.DoSomething( );    }    //Callback runs on worker threads    public void OnCallback( )    {       SendOrPostCallback setText = delegate                                    {                                       Text = "Manually marshaling to UI thread";                                    };       m_Context.Post(setText,null);    }    public void OnClose(object sender,EventArgs args)    {       m_Proxy.Close( );    } } ////////////////////////// Service Side ///////////////////// [ServiceContract(CallbackContract = typeof(IMyContractCallback))] interface IMyContract {    [OperationContract]    void DoSomething( ); } interface IMyContractCallback {    [OperationContract]    void OnCallback( ); } [ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Reentrant)] class MyService : IMyContract {    public void DoSomething( )    {       IMyContractCallback callback = OperationContext.Current.                                          GetCallbackChannel<IMyContractCallback>( );       callback.OnCallback( );    } } 

As shown in Example 8-22, you must use the Post( ) method of the synchronization context. Under no circumstances should you use the Send( ) methodeven though the callback is executing on the worker thread, the UI thread is still blocked on the outbound call. Calling Send( ) would trigger the deadlock you are trying to avoid because Send( ) will block until the UI thread can process the request. Much the same way, the callback in Example 8-22 cannot use any of the safe controls (such as SafeLabel) because those too use the Send( ) method.

8.9.2. Callback Custom Synchronization Context

Similar to a service custom synchronization context, you can install a custom synchronization context for the use of the callback. All that is required is that the thread that opens the proxy (or calls it for the first time) has the custom synchronization context attached to it. Example 8-23 shows how to attach my AffinitySynchronizer class to the callback object by attaching it before using the proxy.

Example 8-23. Setting custom synchronization context for the callback

 interface IMyContractCallback {    [OperationContract]    void OnCallback( ); } class MyClient : IMyContractCallback {    //This method always invoked by the same thread    public void OnCallback( )    {....} } MyClient client = new MyClient( ); InstanceContext callbackContext = new InstanceContext(client); MyContractClient proxy = new MyContractClient(callbackContext); SynchronizationContext synchronizationContext = new AffinitySynchronizer( ); SynchronizationContext.SetSynchronizationContext(synchronizationContext); using(synchronizationContext as IDisposable) {    proxy.DoSomething( );    /* Some blocking operations till after the callback*/    proxy.Close( ); } 

8.9.2.1. The CallbackThreadAffinityBehaviorAttribute

Much the same way as the service, you can also assign a custom synchronization context for a callback endpoint using an attribute. For example, here is my CallbackThreadAffinityBehaviorAttribute:

 [AttributeUsage(AttributeTargets.Class)] public class CallbackThreadAffinityBehaviorAttribute : Attribute,IEndpointBehavior {    public CallbackThreadAffinityBehaviorAttribute(Type callbackType);    public CallbackThreadAffinityBehaviorAttribute(Type callbackType,                                                   string threadName);    public string ThreadName    {get;set;} } 

The CallbackThreadAffinityBehavior attribute makes all callbacks across all callback contracts the client supports execute on the same thread. Since the attribute needs to affect the callback endpoints, it implements the IEndpointBehavior interface presented in Chapter 6. You apply the attribute directly on the callback type, as shown in Example 8-24.

Example 8-24. Applying the CallbackThreadAffinityBehavior attribute

 [CallbackThreadAffinityBehavior(typeof(MyClient))] class MyClient : IMyContractCallback,IDisposable {    MyContractClient m_Proxy;    public void CallService( )    {       InstanceContext callbackContext = new InstanceContext(this);       m_Proxy = new MyContractClient(callbackContext);       m_Proxy.DoSomething( );    }    //This method invoked by same callback thread, plus client threads    public void OnCallback( )    {       //Access state and resources, synchronize manually    }    public void Dispose( )    {       m_Proxy.Close( );    } } 

The attribute requires as a construction parameter the type of the callback it is decorating. Note that although the callback is always invoked by WCF on the same thread, you still may need to synchronize access to it if other client-side threads access the method as well.

Using the CallbackThreadAffinityBehavior attribute as in Example 8-24, Example 8-23 is reduced to:

 MyClient client = new MyClient( ); InstanceContext callbackContext = new InstanceContext(client); MyContractClient proxy = new MyContractClient(callbackContext); proxy.DoSomething( ); /* Some blocking operations till after the callback*/ proxy.Close( ); 

Example 8-25 shows the implementation of the CallbackThreadAffinityBehavior attribute.

Example 8-25. Implementing CallbackThreadAffinityBehaviorAttribute

 [AttributeUsage(AttributeTargets.Class)] public class CallbackThreadAffinityBehaviorAttribute : Attribute,IEndpointBehavior {    string m_ThreadName;    Type m_CallbackType;    public string ThreadName //Accesses m_ThreadName    {get;set;}    public CallbackThreadAffinityBehaviorAttribute(Type callbackType)                                                           : this(callbackType,null)    {}    public CallbackThreadAffinityBehaviorAttribute(Type callbackType,                                                   string threadName)    {       m_ThreadName = threadName;       m_CallbackType = callbackType;       AppDomain.CurrentDomain.ProcessExit += delegate                                              {                                   ThreadAffinityHelper.CloseThread(m_CallbackType);                                              };    }    void IEndpointBehavior.ApplyClientBehavior(ServiceEndpoint serviceEndpoint,                                               ClientRuntime clientRuntime)    {       m_ThreadName = m_ThreadName ?? "Executing callbacks of " + m_CallbackType;       ThreadAffinityHelper.ApplyDispatchBehavior(m_CallbackType,m_ThreadName,                                             clientRuntime.CallbackDispatchRuntime);    }    void IEndpointBehavior.AddBindingParameters(...)    {}    void IEndpointBehavior.ApplyDispatchBehavior(...)    {}    void IEndpointBehavior.Validate(...)    {} } 

The constructor of CallbackThreadAffinityBehavior attribute saves in member variables the supplied callback type and the thread name, if any. The attribute uses the ThreadAffinityHelper class presented earlier to attach the AffinitySynchronizer to the dispatcher. Unlike the service, there is no host involved, so there is no closing event you can subscribe to as a signal to shut down the worker thread of AffinitySynchronizer. Instead, the constructor uses an anonymous method to subscribe to the process exit event of the current app domain. When the client application shuts down, the anonymous method will use ThreadAffinityHelper to close the worker thread.

While the attribute supports IEndpointBehavior, the only method of interest here is ApplyClientBehavior( ) where you can affect the callback endpoint dispatcher:

 public interface IEndpointBehavior {    void ApplyClientBehavior(ServiceEndpoint serviceEndpoint,                             ClientRuntime clientRuntime);    //More members } 

In ApplyClientBehavior( ) the attribute extracts from the ClientRuntime parameter its dispatcher in the CallbackDispatchRuntime parameter:

 public sealed class ClientRuntime {    public DispatchRuntime CallbackDispatchRuntime    {get;}    //More members } 

and passes it to THReadAffinityHelper to attach the AffinitySynchronizer.

While the client-side worker thread used by AffinitySynchronizer will automatically be closed once the client application shuts down, the client may want to expedite that and close it sooner. To that end, the client can explicitly call the CloseThread( ) method of THReadAffinityHelper:

 MyClient client = new MyClient( ); InstanceContext callbackContext = new InstanceContext(client); MyContractClient proxy = new MyContractClient(callbackContext); proxy.DoSomething( ); /* Some blocking operations till after the callback proxy.Close( ); ThreadAffinityHelper.CloseThread(typeof(MyClient)); 




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