Incoming service calls execute on worker threads from the I/O completion thread pool and are unrelated to any service or resource threads. This means that by default the service cannot rely on any kind of thread affinity (that is, always being accessed by the same thread). Much the same way, the service cannot by default rely on executing on any 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 interface resources updated by the service must execute and be accessed only by the user interface (UI) thread. Other examples are 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), or accessing components developed using legacy Visual Basic or Visual FoxPro, which also require thread affinity (due to their own use of the TLS). In addition, for scalability and throughput purposes, some resources or frameworks may require 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.
.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 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. They are simply the synchronizational context of the call.
While conceptually synchronization contexts are a simple enough design pattern to use, implementing a synchronization context is a complex programming task that is not normally intended for developers to attempt.
The SynchronizationContext
class 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 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, 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 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.
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 in the other synchronization context, while Post(
)
will 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, you first somehow obtain a reference to that synchronization context, and then
use the Send( )
method:
//Obtain synchronization context SynchronizationContext context = ... SendOrPostCallback doWork = (arg)=> { //The code here is guaranteed to //execute on the 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 Example 8-4, 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, 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 that the resource is sensitive to its synchronization context, obtain the context, and manage the execution. You must also duplicate such code in any service using the resource. 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( ) {...} } 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.
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 applies equally 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 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
supports the synchronization context pattern. Every thread that pumps Windows messages has
a synchronization context. That synchronization context is the WindowsFormsSynchronizationContext
class:
public sealed class WindowsFormsSynchronizationContext : SynchronizationContext,... {...}
Whenever you create any Windows Forms control or form, that control or form ultimately
derives from the class Control
. The constructor of
Control
checks whether the current thread that
creates it already has a synchronization context, and if it dos not, Control
installs WindowsFormsSynchronizationContext
as the current thread's synchronization
context.
WindowsFormsSynchronizationContext
converts the
call to Send( )
or Post(
)
to a custom Windows message and posts that Windows message to the UI
thread's message queue. Every Windows Forms UI class that derives from Control
has a dedicated method that handles this custom
message by invoking the supplied SendOrPostCallback
delegate. At some point, the UI thread processes the custom Windows message 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.
When a service needs to update a user interface, it must have some proprietary mechanisms to find the window to update in the first place. And 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; public SynchronizationContext MySynchronizationContext {get;set;} public MyForm( ) { InitializeComponent( ); MySynchronizationContext = SynchronizationContext.Current; } void InitializeComponent( ) { ... m_CounterLabel = new Label( ); ... } public int Counter { get { return Convert.ToInt32(m_CounterLabel.Text); } set { m_CounterLabel.Text = value.ToString( ); } } } [ServiceContract] interface IFormManager { [OperationContract] void IncrementLabel( ); } 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
, which provides the MySynchronizationContext
property that allows 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 constructor of MyForm
is called after the
constructor of its topmost base class, Control
, was
called, and Control
has already attached the Windows
Forms synchronization context to the thread in its constructor.
MyForm
also offers a Counter
property that updates the value of a counting Windows Forms label.
Only the thread that owns the form can access that label. 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
property and calls the Send( )
method. Send( )
is provided with
an anonymous method that accesses the Counter
property. Example 8-6 is a concrete example
of the programming model shown in Example 8-4, and it suffers from the same
deficiency: namely, tight coupling between all service operations 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.
A better approach is 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 thread-safe access to its Text
property. Because SafeLabel
derives from Label
, you still
have full design-time visual experience and integration with Visual Studio, yet you can
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 = (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's 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 = newSafe
Label( ); ... } public int Counter { get { return Convert.ToInt32(m_CounterLabel.Text); } set { m_CounterLabel.Text = value.ToString( ); } } } 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.
ServiceModelEx contains not only SafeLabel
but also other controls you are likely to update at runtime
such as SafeButton
, SafeListBox
, SafeProgressBar
, SafeStatusBar
, SafeTrackBar
, and SafeTextBox
.
3.145.35.194