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.
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) { ThreadStart invoke = delegate { m_Proxy.MyMethod( ); }; 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( ); } }
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.
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 = 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 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.
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 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)); } }
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.
18.117.230.81