Callbacks and Synchronization Contexts

Like a service invocation, a 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 Example 8-4 and Example 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 client is using the channel factory, the synchronization context to use is locked in when the client calls CreateChannel(). 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 made on the proxy (or the call to Open() or CreateChannel()) 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.

Callbacks and the 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 Example 8-6 or Example 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-22.

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

partial class MyForm : Form,IMyContractCallback
{
   MyContractClient m_Proxy;

   public MyForm()
   {
      InitializeComponent();
      m_Proxy = new MyContractClient(new InstanceContext(this));
   }
   //Called as a result of a UI event
   public void OnCallService(object sender,EventArgs args)
   {
      m_Proxy.MyMethod(); //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-22 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-23.

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

partial class MyForm : Form,IMyContractCallback
{
   MyContractClient m_Proxy;

   public MyForm()
   {
      InitializeComponent();

      m_Proxy = new MyContractClient(new InstanceContext(this));

      //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)
   {
      Thread thread = new Thread(()=>m_Proxy.MyMethod());
      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();
   }
}

UI thread callbacks and responsiveness

When callbacks are being processed on the UI thread, the UI itself is not responsive. Even if you perform relatively short callbacks, you must bear in mind that if the callback class is configured with ConcurrencyMode.Multiple there may 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 class with ConcurrencyMode.Single so that the callback lock will queue up the callbacks. They can then be dispatched to the callback object one at a time, giving them the chance of being interleaved among the UI messages.

UI thread callbacks and concurrency management

Configuring the callback for affinity to the UI thread may trigger a deadlock. Suppose a Windows Forms client establishes an affinity between a callback object (or even itself) and the UI synchronization context, and then calls a service, passing the callback reference. The service is configured for reentrancy, and it calls back to the client. A deadlock now occurs 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. For example, Example 8-22 has the potential for this deadlock. 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-24 demonstrates using this technique.

Example 8-24. Avoiding a 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;
      m_Proxy = new MyContractClient(new InstanceContext(this));
   }

   public void CallService(object sender,EventArgs args)
   {
      m_Proxy.MyMethod();
   }
   //Callback runs on worker threads
   public void OnCallback()
   {
      SendOrPostCallback setText = _=>
                                   {
                                      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 MyMethod();
}
interface IMyContractCallback
{
   [OperationContract]
   void OnCallback();
}
[ServiceBehavior(ConcurrencyMode = ConcurrencyMode.Reentrant)]
class MyService : IMyContract
{
   public void MyMethod()
   {
      IMyContractCallback callback = OperationContext.Current.
                                        GetCallbackChannel<IMyContractCallback>();
      callback.OnCallback();
   }
}

As shown in Example 8-24, you must use the Post() method of the synchronization context. Under no circumstances should you use the Send() method—even 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. The callback in Example 8-24 cannot use any of the safe controls (such as SafeLabel) either, because those too use the Send() method.

Callback Custom Synchronization Contexts

As with a service, 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-25 shows how to attach my ThreadPoolSynchronizer to the callback object by setting it before using the proxy.

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

interface IMyContractCallback
{
   [OperationContract]
   void OnCallback();
}
[ServiceContract(CallbackContract = typeof(IMyContractCallback))]
interface IMyContract
{
   [OperationContract]
   void MyMethod();
}

class MyClient : IMyContractCallback
{
   //This method is 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 ThreadPoolSynchronizer(3);
SynchronizationContext.SetSynchronizationContext(synchronizationContext);

using(synchronizationContext as IDisposable)
{
   proxy.MyMethod();
   /*Some blocking operations until after the callback*/
   proxy.Close();
}

While you could manually install a custom synchronization context (as in Example 8-25) by explicitly setting it before opening the proxy, it is better to do so declaratively, using an attribute. To affect the callback endpoint dispatcher, the attribute needs to implement the IEndpointBehavior interface presented in Chapter 6:

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

In the ApplyClientBehavior method, the ClientRuntime parameter contains a reference to the endpoint dispatcher with the CallbackDispatchRuntime property:

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

The rest is identical to the service-side attribute, as demonstrated by my CallbackThreadPoolBehaviorAttribute, whose implementation is shown in Example 8-26.

Example 8-26. Implementing CallbackThreadPoolBehaviorAttribute

[AttributeUsage(AttributeTargets.Class)]
public class CallbackThreadPoolBehaviorAttribute : ThreadPoolBehaviorAttribute,
                                                                  IEndpointBehavior
{
   public CallbackThreadPoolBehaviorAttribute(uint poolSize,Type clientType) :
                                                     this(poolSize,clientType,null)
   {}
   public CallbackThreadPoolBehaviorAttribute(uint poolSize,Type clientType,
                              string poolName) : base(poolSize,clientType,poolName)
   {
      AppDomain.CurrentDomain.ProcessExit += delegate
                                             {
                                        ThreadPoolHelper.CloseThreads(ServiceType);
                                             };
   }
   void IEndpointBehavior.ApplyClientBehavior(ServiceEndpoint serviceEndpoint,
                                              ClientRuntime clientRuntime)
   {
      IContractBehavior contractBehavior = this;
      contractBehavior.ApplyDispatchBehavior(null,serviceEndpoint,
                                            clientRuntime.CallbackDispatchRuntime);
   }
   //Rest of the implementation
}

In fact, I wanted to reuse as much of the service attribute as possible in the callback attribute. To that end, CallbackThreadPoolBehaviorAttribute derives from ThreadPoolBehaviorAttribute. Its constructors pass the client type as the service type to the base constructors. The CallbackThreadPoolBehavior attribute’s implementation of ApplyClientBehavior() queries its base class for IContractBehavior (this is how a subclass uses an explicit private interface implementation of its base class) and delegates the implementation to ApplyDispatchBehavior().

The big difference between a client callback attribute and a service attribute is that the callback scenario has no host object to subscribe to its Closed event. To compensate, the CallbackThreadPoolBehavior attribute monitors the process exit event to close all the threads in the pool.

If the client wants to expedite closing those threads, it can use ThreadPoolBehavior.CloseThreads(), as shown in Example 8-27.

Example 8-27. Using the CallbackThreadPoolBehavior attribute

interface IMyContractCallback
{
   [OperationContract]
   void OnCallback();
}

[ServiceContract(CallbackContract = typeof(IMyContractCallback))]
interface IMyContract
{
   [OperationContract]
   void MyMethod();
}

[CallbackThreadPoolBehavior(3,typeof(MyClient))]
class MyClient : IMyContractCallback,IDisposable
{
   MyContractClient m_Proxy;

   public MyClient()
   {
      m_Proxy = new MyContractClient(new InstanceContext(this));
   }

   public void CallService()
   {
      m_Proxy.MyMethod();
   }

   //Called by threads from the custom pool
   public void OnCallback()
   {...}

   public void Dispose()
   {
      m_Proxy.Close();
      ThreadPoolHelper.CloseThreads(typeof(MyClient));
   }
}

Callback thread affinity

Just like on the service side, if you want all the callbacks to execute on the same thread (perhaps to create some UI on the callback side), you can configure the callback class to have a pool size of 1. Or, better yet, you can define a dedicated callback attribute such as my CallbackThreadAffinityBehaviorAttribute:

[AttributeUsage(AttributeTargets.Class)]
public class CallbackThreadAffinityBehaviorAttribute :
                                                CallbackThreadPoolBehaviorAttribute
{
   public CallbackThreadAffinityBehaviorAttribute(Type clientType) :
                                          this(clientType,"Callback Worker Thread")
   {}
   public CallbackThreadAffinityBehaviorAttribute(Type clientType,
                                 string threadName) : base(1,clientType,threadName)
   {}
}

The CallbackThreadAffinityBehavior attribute makes all callbacks across all callback contracts the client supports execute on the same thread, as shown in Example 8-28.

Example 8-28. Applying the CallbackThreadAffinityBehavior attribute

[CallbackThreadAffinityBehavior(typeof(MyClient))]
class MyClient : IMyContractCallback,IDisposable
{
   MyContractClient m_Proxy;

   public void CallService()
   {
      m_Proxy = new MyContractClient(new InstanceContext(this));
      m_Proxy.MyMethod();
   }
   //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();
   }
}

Note that although WCF always invokes the callback on the same thread, you still may need to synchronize access to it if other client-side threads access the method as well.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.128.78.30