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 synchronization contexts are a simple enough design pattern to use conceptually, 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. Instead of an object
,
you can use anonymous methods and outer variables (closures) that
are type-safe.
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 = _=> { 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 a Lambda expression that
wraps the call to DoWork()
and
assigns that expression 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 Lambda expression assigns the
returned value of DoWork()
into
an outer 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 = _=> { 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 best way to illustrate the usefulness and power of synchronization contexts, along with concrete examples of the abstract patterns discussed so far, is when utilizing synchronization contexts along with Windows user interface frameworks such as Windows Forms or the Windows Presentation Foundation (WPF). Note that while this discussion focuses on the UI case, the patterns, design guidelines, and consequences of those design decisions and even the best practices apply to most other cases of a synchronization context.
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 Windows
Forms
Synchronization
Context
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 does
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 readonly SynchronizationContext MySynchronizationContext; 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 = _=> { 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 { m_SynchronizationContext.Send(_=> base.Text = value,null); } get { string text = String.Empty; m_SynchronizationContext.Send(_=> text = base.Text,null); return text; } } }
Upon construction, SafeLabel
caches its synchronization
context. SafeLabel
overrides its
base class’s Text
property and
uses the Lambda expression 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 Safe
Label
, 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 = new Safe
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
.
Exercise caution when using the safe controls (or in the general case, safe resources that encapsulate their own synchronization context). While having safe resources does simplify accessing each individual resource, if you have to access multiple resources, you will pay the penalty of marshaling to the synchronization context with every one of them. With multiple resources, it is better to lump all the accesses into a single method and marshal just the call to that method to the target synchronization context.
3.128.78.30