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 Use
Synchronization
Context
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) { 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(); } }
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 = _=> { 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 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 Thread
Pool
Behavior.
Close
Threads()
, 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.
3.128.78.30