When a client calls a service, usually the client is blocked while the service executes the call, and control returns to the client only when the operation completes its execution and returns. However, there are quite a few cases in which you will want to call operations asynchronously; that is, you'll want control to return immediately to the client while the service executes the operation in the background and then somehow let the client know that the method has completed execution and provide the client with the results of the invocation. Such an execution mode is called asynchronous operation invocation, and the action is known as an asynchronous call. Asynchronous calls allow you to improve client responsiveness and availability.
To make the most of the various options available with WCF asynchronous calls, you should be aware of the generic requirements set for any service-oriented asynchronous call support. These requirements include the following:
The same service code should be used for both synchronous and asynchronous invocation. This allows service developers to focus on business logic and cater to both synchronous and asynchronous clients.
A corollary of the first requirement is that the client should be the one to decide whether to call a service synchronously or asynchronously. That, in turn, implies that the client will have different code for each case (whether to invoke the call synchronously or asynchronously).
The client should be able to issue multiple asynchronous calls and have multiple asynchronous calls in progress, and it should be able to distinguish between multiple methods' completions.
Since a service operation's output parameters and return values are not available when control returns to the client, the client should have a way to harvest the results when the operation completes.
Similarly, communication errors or errors on the service side should be communicated back to the client side. Any exception thrown during operation execution should be played back to the client later.
The implementation of the mechanism should be independent of the binding and transfer technology used. Any binding should support asynchronous calls.
The mechanism should not use technology-specific constructs such as .NET exceptions or delegates.
The asynchronous calls mechanism should be straightforward and simple to use (this is less of a requirement and more of a design guideline). For example, the mechanism should, as much as possible, hide its implementation details, such as the worker threads used to dispatch the call.
The client has a variety of options for handling operation completion. After it issues an asynchronous call, it can choose to:
Perform some work while the call is in progress and then block until completion.
Perform some work while the call is in progress and then poll for completion.
Receive notification when the method has completed. The notification will be in the form of a callback on a client-provided method. The callback should contain information identifying which operation has just completed and its return values.
Perform some work while the call is in progress, wait for a predetermined amount of time, and then stop waiting, even if the operation execution has not yet completed.
Wait simultaneously for completion of multiple operations. The client can also choose to wait for all or any of the pending calls to complete.
WCF offers all of these options to clients. The WCF support is strictly a client-side facility, and in fact the service is unaware it is being invoked asynchronously. This means that intrinsically any service supports asynchronous calls, and that you can call the same service both synchronously and asynchronously. In addition, because all of the asynchronous invocation support happens on the client side regardless of the service, you can use any binding for the asynchronous invocation.
The WCF asynchronous calls support presented in this section is similar but not identical to the delegate-based asynchronous calls support .NET offers for regular CLR types.
Because the client decides if the call should be synchronous or asynchronous, you need to create a different proxy for the asynchronous case. In Visual Studio 2008, when adding a service reference, you can click the Advanced button in the Add Service Reference dialog to bring up the settings dialog that lets you tweak the proxy generation. Check the "Generate asynchronous operations" checkbox to generate a proxy that contains asynchronous methods in addition to the synchronous ones. For each operation in the original contract, the asynchronous proxy and contract will contain two additional methods of this form:
[OperationContract(AsyncPattern = true
)]
IAsyncResult Begin<Operation>(<in arguments>,
AsyncCallback callback,object asyncState);
<returned type> End<Operation>(<out arguments>,IAsyncResult result);
The OperationContract
attribute offers the AsyncPattern
Boolean property, defined as:
[AttributeUsage(AttributeTargets.Method)] public sealed class OperationContractAttribute : Attribute { public bool AsyncPattern {get;set;} //More members }
The AsyncPattern
property defaults to false
. AsyncPattern
has
meaning only on the client side; it is merely a validation flag indicating to the proxy to
verify that the method on which this flag is set to true
has a Begin<Operation>(
)
-compatible signature and that the defining contract has a matching method with
an End<Operation>( )
-compatible signature. These
requirements are verified at the proxy load time. AsyncPattern
binds the underlying synchronous method with the Begin
/End
pair and
correlates the synchronous execution with the asynchronous one. Briefly, when the client
invokes a method of the form Begin<Operation>( )
with AsyncPattern
set to true
, this tells WCF not to try to directly invoke a method with that name on
the service. Instead, WCF should use a thread from the thread pool to synchronously call
the underlying method. The synchronous call will block the thread from the thread pool,
not the calling client. The client will be blocked for only the slightest moment it takes
to dispatch the call request to the thread pool. The reply method of the synchronous
invocation is correlated with the End<Operation>(
)
method.
Example 8-29 shows a calculator contract and its implementing service, and the generated asynchronous proxy.
Example 8-29. Asynchronous contract and proxy
////////////////////////// Service Side //////////////////////
[ServiceContract]
interface ICalculator
{
[OperationContract]
int Add(int number1,int number2);
//More operations
}
class Calculator : ICalculator
{
public int Add(int number1,int number2)
{
return number1 + number2;
}
//Rest of the implementation
}
////////////////////////// Client Side //////////////////////
[ServiceContract]
public interface ICalculator
{
[OperationContract]
int Add(int number1,int number2);
[OperationContract(AsyncPattern = true
)]
IAsyncResult BeginAdd(int number1,int number2,
AsyncCallback callback,object asyncState);
int EndAdd(IAsyncResult result);
//Rest of the methods
}
partial class CalculatorClient : ClientBase<ICalculator>,ICalculator
{
public int Add(int number1,int number2)
{
return Channel.Add(number1,number2);
}
public IAsyncResult BeginAdd(int number1,int number2,
AsyncCallback callback,object asyncState)
{
return Channel.BeginAdd(number1,number2,callback,asyncState);
}
public int EndAdd(IAsyncResult result)
{
return Channel.EndAdd(result);
}
//Rest of the methods and constructors
}
Begin<Operation>( )
accepts the input
parameters of the original synchronous operation, which may include data contracts passed
by value or by reference (using the ref
modifier). The
original method's return values and any explicit output parameters (designated using the
out
and ref
modifiers) are part of the End<Operation>( )
method. For example, for this operation definition:
[OperationContract] string MyMethod(int number1,out int number2,ref int number3);
the corresponding Begin<Operation>( )
and
End<Operation>( )
methods look like
this:
[ServiceOperation(AsyncPattern = true)] IAsyncResult BeginMyMethod(int number1,ref int number3, AsyncCallback callback,object asyncState); string EndMyMethod(out int number2,ref int number3,IAsyncResult result);
Begin<Operation>( )
accepts two additional
input parameters that are not present in the original operation signature: callback
and asyncState
.
The callback
parameter is a delegate targeting a
client-side method-completion notification event. asyncState
is an object that conveys whatever state information the party
handling the method completion requires. These two parameters are optional: the caller can
choose to pass in null
instead of either one of them.
For example, you could use code like the following to asynchronously invoke the Add( )
method of the Calculator
service from Example 8-29
using the asynchronous proxy, if you have no interest in the results or the errors:
CalculatorClient proxy = new CalculatorClient( ); proxy.BeginAdd(2,3,null,null); //Dispatched asynchronously proxy.Close( );
As long as the client has the definition of the asynchronous contract, you can also invoke the operation asynchronously using a channel factory:
ChannelFactory<ICalculator> factory = new ChannelFactory<ICalculator>( ); ICalculator proxy = factory.CreateChannel( ); proxy.BeginAdd(2,3,null,null); ICommunicationObject channel = proxy as ICommunicationObject; channel.Close( );
The problem with such an invocation is that the client has no way of getting its results.
Every Begin<Operation>( )
method returns an
object implementing the IAsyncResult
interface,
defined in the System.Runtime.Remoting.Messaging
namespace as:
public interface IAsyncResult { object AsyncState {get;} WaitHandle AsyncWaitHandle {get;} bool CompletedSynchronously {get;} bool IsCompleted {get;} }
The returned IAsyncResult
implementation uniquely
identifies the method that was invoked using Begin<Operation>( )
. You can pass the IAsyncResult
-implementation object to End<Operation>( )
to identify the specific asynchronous method
execution from which you wish to retrieve the results. End<Operation>( )
will block its caller until the operation it's
waiting for (identified by the IAsyncResult
-implementation object passed in) completes and it can return the
results or errors. If the method is already complete by the time End<Operation>( )
is called, End<Operation>( )
will not block the caller and will just return the
results. Example 8-30 shows the entire
sequence.
Example 8-30. Simple asynchronous execution sequence
CalculatorClient proxy = new CalculatorClient( ); IAsyncResult result1 = proxy.BeginAdd(2,3,null,null); IAsyncResult result2 = proxy.BeginAdd(4,5,null,null); /* Do some work */ int sum; sum = proxy.EndAdd(result1); //This may block Debug.Assert(sum == 5); sum = proxy.EndAdd(result2); //This may block Debug.Assert(sum == 9); proxy.Close( );
As simple as Example 8-30 is, it does
demonstrate a few key points. The first point is that the same proxy instance can invoke
multiple asynchronous calls. The caller can distinguish among the different pending
calls using each unique IAsyncResult
-implementation
object returned from Begin<Operation>( )
. In
fact, when the caller makes asynchronous calls, as in Example 8-30, it must save the IAsyncResult
-implementation objects. In addition, the caller
should make no assumptions about the order in which the pending calls will complete. It
is quite possible that the second call will complete before the first one.
Although it isn't evident in Example 8-30, there are two important programming points regarding asynchronous calls:
End<Operation>( )
can be called only
once for each asynchronous operation. Trying to call it more than once results in an
InvalidOperationException
.
You can pass the IAsyncResult
-implementation
object to End<Operation>( )
only on the
same proxy object used to dispatch the call. Passing the IAsyncResult
-implementation object to a different proxy instance
results in an AsyncCallbackException
. This is
because only the original proxy keeps track of the asynchronous operations it has
invoked.
If the proxy is not using a transport session, the client can close the proxy
immediately after the call to Begin<Operation>(
)
and still be able to call End<Operation>(
)
later:
CalculatorClient proxy = new CalculatorClient( );
IAsyncResult result = proxy.BeginAdd(2,3,null,null);
proxy.Close( );
/*Do some work */
//Sometime later:
int sum = proxy.EndAdd(result);
Debug.Assert(sum == 5);
When a client calls End<Operation>( )
, the
client is blocked until the asynchronous method returns. This may be fine if the client
has a finite amount of work to do while the call is in progress, and if after completing
that work the client cannot continue its execution without the returned value or the
output parameters of the operation. However, what if the client only wants to check that
the operation has completed? What if the client wants to wait for completion for a fixed
timeout and then, if the operation has not completed, do some additional finite processing
and wait again? WCF supports these alternative programming models to calling End<Operation>( )
.
The IAsyncResult
interface object returned from
Begin<Operation>( )
has the AsyncWaitHandle
property, of type WaitHandle
:
public abstract class WaitHandle : ... { public static bool WaitAll(WaitHandle[] waitHandles); public static int WaitAny(WaitHandle[] waitHandles); public virtual void Close( ); public virtual bool WaitOne( ); //More memebrs }
The WaitOne( )
method of WaitHandle
returns only when the handle is signaled. Example 8-31 demonstrates using WaitOne( )
.
Example 8-31. Using IAsyncResult.AsyncWaitHandle to block until completion
CalculatorClient proxy = new CalculatorClient( ); IAsyncResult result = proxy.BeginAdd(2,3,null,null); /* Do some work */ result.AsyncWaitHandle.WaitOne( ); //This may block int sum = proxy.EndAdd(result); //This will not block Debug.Assert(sum == 5); proxy.Close( );
Logically, Example 8-31 is identical to
Example 8-30, which called only End<Operation>( )
. If the operation is still executing
when WaitOne( )
is called, WaitOne( )
will block. But if by the time WaitOne(
)
is called the method execution is complete, WaitOne( )
will not block, and the client will proceed to call End<Operation>( )
for the returned value. The important
difference between Example 8-31 and Example 8-30 is that the call to End<Operation>( )
in Example 8-31 is guaranteed not to block its
caller.
Example 8-32 demonstrates a more
practical way of using WaitOne( )
, by specifying a
timeout (10 milliseconds in this example). When you specify a timeout, WaitOne( )
returns when the method execution is completed or
when the timeout has elapsed, whichever condition is met first.
Example 8-32. Using WaitOne( ) to specify wait timeout
CalculatorClient proxy = new CalculatorClient( );
IAsyncResult result = proxy.BeginAdd(2,3,null,null);
while(result.IsCompleted == false)
{
result.AsyncWaitHandle.WaitOne(10
,false); //This may block
/* Do some optional work */
}
int sum = proxy.EndAdd(result); //This will not block
Example 8-32 uses another handy property
of IAsyncResult
, called IsCompleted
. IsCompleted
lets you check
the status of the call without waiting or blocking. You can even use IsCompleted
in a strict polling mode:
CalculatorClient proxy = new CalculatorClient( ); IAsyncResult result = proxy.BeginAdd(2,3,null,null); //Sometime later: if(result.IsCompleted) { int sum = proxy.EndAdd(result); //This will not block Debug.Assert(sum == 5); } else { //Do some optional work } proxy.Close( );
The AsyncWaitHandle
property really shines when you
use it to manage multiple concurrent asynchronous methods in progress. You can use
WaitHandle
's static WaitAll(
)
method to wait for completion of multiple asynchronous methods, as shown in
Example 8-33.
Example 8-33. Waiting for completion of multiple methods
CalculatorClient proxy = new CalculatorClient( );
IAsyncResult result1 = proxy.BeginAdd(2,3,null,null);
IAsyncResult result2 = proxy.BeginAdd(4,5,null,null);
WaitHandle[] handleArray = {result1.AsyncWaitHandle,result2.AsyncWaitHandle};
WaitHandle.WaitAll
(handleArray);
int sum;
//These calls to EndAdd( ) will not block
sum = proxy.EndAdd(result1);
Debug.Assert(sum == 5);
sum = proxy.EndAdd(result2);
Debug.Assert(sum == 9);
proxy.Close( );
To use WaitAll( )
, you need to construct an array
of handles. Note that you still need to call End<Operation>( )
to access the returned values. Instead of waiting for
all of the methods to return, you can choose to wait for any of them to return, using the
WaitAny( )
static method of the WaitHandle
class. Like WaitOne(
)
, both WaitAll( )
and WaitAny( )
have overloaded versions that let you specify a
timeout to wait instead of waiting indefinitely.
Instead of blocking, waiting, and polling for asynchronous call completion, WCF offers
another programming model altogetherācompletion callbacks. With this model, the client
provides WCF with a method and requests that WCF call that method back when the
asynchronous method completes. The client can have the same callback method handle
completion of multiple asynchronous calls. When each asynchronous method's execution is
complete, instead of quietly returning to the pool, the worker thread calls the completion
callback. To designate a completion callback method, the client needs to provide Begin<Operation>( )
with a delegate of the type AsyncCallback
, defined as:
public delegate void AsyncCallback(IAsyncResult result);
That delegate is provided as the penultimate parameter to Begin<Operation>( )
.
Example 8-34 demonstrates asynchronous call management using a completion callback.
Example 8-34. Managing asynchronous call with a completion callback
class MyClient : IDisposable { CalculatorClient m_Proxy = new CalculatorClient( ); public void CallAsync( ) { m_Proxy.BeginAdd(2,3,OnCompletion,null); } void OnCompletion(IAsyncResult result) { int sum = m_Proxy.EndAdd(result); Debug.Assert(sum == 5); } public void Dispose( ) { m_Proxy.Close( ); } }
Unlike the programming models described so far, when you use a completion callback
method, there's no need to save the IAsyncResult
-implementation object returned from Begin<Operation>( )
. This is because when WCF calls the completion
callback, WCF provides the IAsyncResult
-implementation
object as a parameter. Because WCF provides a unique IAsyncResult
-implementation object for each asynchronous method, you can
channel multiple asynchronous method completions to the same callback method:
m_Proxy.BeginAdd(2,3,OnCompletion
,null); m_Proxy.BeginAdd(4,5,OnCompletion
,null);
Instead of using a class method as a completion callback, you can just as easily use a local anonymous method or a lambda expression:
CalculatorClient proxy = new CalculatorClient( ); int sum; AsyncCallback completion = (result)=> { sum = proxy.EndAdd(result); Debug.Assert(sum == 5); proxy.Close( ); }; proxy.BeginAdd(2,3,completion,null);
Note that the anonymous method assigns to an outer variable (sum
) to provide the result of the Add( )
operation.
Callback completion methods are by far the preferred model in any event-driven application. An event-driven application has methods that trigger events (or requests) and methods that handle those events and fire their own events as a result. Writing an application as event-driven makes it easier to manage multiple threads, events, and callbacks and allows for scalability, responsiveness, and performance.
The last thing you want in an event-driven application is to block, since then your application does not process events. Callback completion methods allow you to treat the completion of the asynchronous operation as yet another event in your system. The other options (waiting, blocking, and polling) are available for applications that are strict, predictable, and deterministic in their execution flow. I recommend that you use completion callback methods whenever possible.
Because the callback method is executed on a thread from the thread pool, you must provide for thread safety in the callback method and in the object that provides it. This means that you must use synchronization objects and locks to access the member variables of the client, even outer variables to anonymous completion methods. You need to provide for synchronization between client-side threads and the worker thread from the pool, and potentially synchronizing between multiple worker threads all calling concurrently into the completion callback method to handle their respective asynchronous call completion. Therefore, you need to make sure the completion callback method is reentrant and thread-safe.
The last parameter to Begin<Operation>( )
is asyncState
. The asyncState
object, known as a state object, is
provided as an optional container for whatever need you deem fit. The party handling the
method completion can access such a container object via the AsyncState
property of IAsyncResult
.
Although you can certainly use state objects with any of the other asynchronous call
programming models (blocking, waiting, or polling), they are most useful in conjunction
with completion callbacks. The reason is simple: when you are using a completion
callback, the container object offers the only way to pass in additional parameters to
the callback method, whose signature is predetermined.
Example 8-35 demonstrates how you might
use a state object to pass an integer value as an additional parameter to the completion
callback method. Note that the callback must downcast the AsyncState
property to the actual type.
Example 8-35. Passing an additional parameter using a state object
class MyClient : IDisposable { CalculatorClient m_Proxy = new CalculatorClient( ); public void CallAsync( ) { int asyncState = 4; //int, for example m_Proxy.BeginAdd(2,3,OnCompletion,asyncState
); } void OnCompletion(IAsyncResult result) { int asyncState =(int)
result.AsyncState
; Debug.Assert(asyncState == 4); int sum = m_Proxy.EndAdd(result); } public void Dispose( ) { m_Proxy.Close( ); } }
A common use for the state object is to pass the proxy used for Begin<Operation>( )
instead of saving it as a member
variable:
class MyClient { public void CallAsync( ) { CalculatorClient proxy = new CalculatorClient( ); proxy.BeginAdd(2,3,OnCompletion,proxy
); } void OnCompletion(IAsyncResult result) { CalculatorClient proxy = result.AsyncState
as CalculatorClient; Debug.Assert(proxy != null); int sum = proxy.EndAdd(result); Debug.Assert(sum == 5); proxy.Close( ); } }
The completion callback, by default, is called on a thread from the thread pool.
This presents a serious problem if the callback is to access some resources that have an
affinity to a particular thread or threads and are required to run in a particular
synchronization context. The classic example is a Windows Forms application that
dispatches a lengthy service call asynchronously (to avoid blocking the UI), and then
wishes to update the UI with the result of the invocation. Using the raw Begin<Operation>( )
is disallowed, since only the UI
thread is allowed to update the UI. You must marshal the call from the completion
callback to the correct synchronization context, using any of the techniques described
previously (such as safe controls). Example 8-36 demonstrates such a completion
callback that interacts directly with its containing form, ensuring that the UI update
will be in the UI synchronization context.
Example 8-36. Relying on completion callback synchronization context
partial class CalculatorForm : Form
{
CalculatorClient m_Proxy;
SynchronizationContext m_SynchronizationContext;
public CalculatorForm( )
{
InitializeComponent( );
m_Proxy = new CalculatorClient( );
m_SynchronizationContext = SynchronizationContext.Current;
}
public void CallAsync(object sender,EventArgs args)
{
m_Proxy.BeginAdd(2,3,OnCompletion,null);
}
void OnCompletion(IAsyncResult result)
{
SendOrPostCallback callback = delegate
{
Text = "Sum = " + m_Proxy.EndAdd(result);
};
m_SynchronizationContext.Send(callback,null);
}
public void OnClose(object sender,EventArgs args)
{
m_Proxy.Close( );
}
}
To better handle this situation, the ClientBase<T>
base class in .NET 3.5 is extended with a protected
InvokeAsync( )
method that picks up the
synchronization context of the client and uses it to invoke the completion callback, as
shown in Example 8-37.
Example 8-37. Async callback management in ClientBase<T>
public abstract class ClientBase<T> : ...
{
protected delegate IAsyncResult BeginOperationDelegate(object[] inValues,
AsyncCallback asyncCallback,object state);
protected delegate object[] EndOperationDelegate(IAsyncResult result);
//Picks up sync context and used for completion callback
protected void InvokeAsync
(BeginOperationDelegate beginOpDelegate,
object[] inValues,
EndOperationDelegate endOpDelegate,
SendOrPostCallback opCompletedCallback,
object userState);
//More members
}
ClientBase<T>
also provides an event
arguments helper class and two dedicated delegates used to invoke and end the
asynchronous call. The generated proxy class that derives from ClientBase<T>
makes use of the base functionality. The proxy will
have a public event called <Operation>Completed
that uses a strongly typed event argument class specific to the results of the
asynchronous method, and two methods called <Operation>Async
that are used to dispatch the call
asynchronously:
partial class AddCompletedEventArgs : AsyncCompletedEventArgs
{
public int Result
{get;}
}
class CalculatorClient : ClientBase<ICalculator>,ICalculator
{
public event
EventHandler<AddCompletedEventArgs> AddCompleted;
public void AddAsync(int number1,int number2,object userState);
public void AddAsync(int number1,int number2);
//Rest of the proxy
}
The client can subscribe an event handler to the <Operation>Completed
event to have that handler called upon
completion. The big difference with using <Operation>Async
as opposed to Begin<Operation>
is that the <Operation>Async
methods will pick up the synchronization context of
the client and will fire the <Operation>Completed
event on that synchronization context, as shown
in Example 8-38.
Example 8-38. Synchronization-context-friendly asynchronous call invocation
partial class CalculatorForm : Form
{
CalculatorClient m_Proxy;
public CalculatorForm( )
{
InitializeComponent( );
m_Proxy = new CalculatorClient( );
m_Proxy.AddCompleted += OnAddCompleted;
}
void CallAsync(object sender,EventArgs args)
{
m_Proxy.AddAsync(2,3); //Sync context picked up here
}
//Called on the UI thread
void OnAddCompleted(object sender,AddCompletedEventArgs args)
{
Text = "Sum = " + args.Result;
}
}
There is little sense in trying to invoke a one-way operation asynchronously, because
while one of the main features of asynchronous calls is their ability to retrieve and
correlate a reply message, no such message is available with a one-way call. If you do
invoke a one-way operation asynchronously, End<Operation>(
)
will return as soon as the worker thread has finished dispatching the call.
Aside from communication errors, End<Operation>(
)
will not encounter any exceptions. If a completion callback is provided for
an asynchronous invocation of a one-way operation, the callback is called immediately
after the worker thread used in Begin<Operation>(
)
dispatches the call. The only justification for invoking a one-way operation
asynchronously is to avoid the potential blocking of the one-way call, in which case you
should pass a null
for the state object and the
completion callback, as shown in Example 8-39.
Example 8-39. Invoking a one-way operation asynchronously
[ServiceContract] interface IMyContract { [OperationContract(IsOneWay = true
)] void MyMethod(string text); [OperationContract(IsOneWay = true,AsyncPattern = true)] IAsyncResult BeginMyMethod(string text, AsyncCallback callback,object asyncState); void EndMyMethod(IAsyncResult result); } MyContractClient proxy = MyContractClient( ); proxy.BeginMyMethod("Async one way",null,null
); //Sometime later: proxy.Close( );
The problem with Example 8-39 is the
potential race condition of closing the proxy. It is possible to push the asynchronous
call with Begin<Operation>( )
and then close the
proxy before the worker thread used has had a chance to invoke the call. If you want to
close the proxy immediately after asynchronously invoking the one-way call, you need to
provide a completion method for closing the proxy:
MyContractClient proxy = MyContractClient( ); AsyncCallback completion = (result)=> {proxy.Close( );
}; proxy.BeginMyMethod("Async one way",completion
,null);
Output parameters and return values are not the only elements unavailable at the time
an asynchronous call is dispatched: exceptions are missing as well. After calling Begin<Operation>( )
, control returns to the client, but
it may be some time before the asynchronous method encounters an error and throws an
exception, and some time after that before the client actually calls End<Operation>( )
. WCF must therefore provide some way
for the client to know that an exception was thrown and allow the client to handle it.
When the asynchronous method throws an exception, the proxy catches it, and when the
client calls End<Operation>( )
the proxy rethrows
that exception object, letting the client handle the exception. If a completion callback
is provided, WCF calls that method immediately after the exception is received. The exact
exception thrown is compliant with the fault contract and the exception type, as explained
in Chapter 6.
If fault contracts are defined on the service operation contract, the FaultContract
attribute should be applied only on the
synchronous operations.
Since the asynchronous invocation mechanism is nothing but a convenient programming
model on top of the actual synchronous operation, the underlying synchronous call can
still time out. This will result in a TimeoutException
when the client calls End<Operation>( )
. It is therefore wrong to equate asynchronous calls
with lengthy operations. By default, asynchronous calls are still relatively short
(under a minute), but unlike synchronous calls, they are non-blocking. For lengthy
asynchronous calls you will need to provide an adequately long send timeout.
When the client calls Begin<Operation>( )
,
the returned IAsyncResult
will have a reference to a
single WaitHandle
object, accessible via the AsyncWaitHandle
property. Calling End<Operation>( )
on that object will not close the handle. Instead,
that handle will be closed when the implementing object is garbage-collected. As with
any other case of using an unmanaged resource, you have to be mindful about your
application-deterministic finalization needs. It is possible (in theory, at least) for
the application to dispatch asynchronous calls faster than .NET can collect the handles,
resulting in a resource leak. To compensate, you can explicitly close that handle after
calling End<Operation>( )
. For example, using
the same definitions as those in Example 8-34:
void OnCompletion(IAsyncResult result)
{
int sum = m_Proxy.EndAdd(result);
Debug.Assert(sum == 5);
result.AsyncWaitHandle.Close( );
}
Transactions do not mix well with asynchronous calls, for a few reasons. First, well-designed transactions are of short duration, yet the main motivation for using asynchronous calls is because of the latency of the operations. Second, the client's ambient transaction will not by default flow to the service, because the asynchronous operation is invoked on a worker thread, not the client's thread. While it is possible to develop a proprietary mechanism that uses cloned transactions, this is esoteric at best and should be avoided. Finally, when a transaction completes, it should have no leftover activities to do in the background that could commit or abort independently of the transaction; however, this will be the result of spawning an asynchronous operation call from within a transaction. In short, do not mix transactions with asynchronous calls.
Although it is technically possible to call the same service synchronously and asynchronously, the likelihood that a service will be accessed both ways is low.
The reason is that using a service asynchronously necessitates drastic changes to the
workflow of the client, and consequently the client cannot simply use the same execution
sequence logic as with synchronous access. Consider, for example, an online store
application. Suppose the client (a server-side object executing a customer request)
accesses a Store
service, where it places the
customer's order details. The Store
service uses three
well-factored helper services to process the order: Order
, Shipment
, and Billing
. In a synchronous scenario, the Store
service first calls the Order
service to place the order. Only if the Order
service succeeds in processing the order (i.e., if the item is
available in the inventory) does the Store
service then
call the Shipment
service, and only if the Shipment
service succeeds does the Store
service access the Billing
service
to bill the customer. This sequence is shown in Figure 8-4.
The downside to the workflow shown in Figure 8-4 is that the store must process orders
synchronously and serially. On the surface, it might seem that if the Store
service invoked its helper objects asynchronously, it
would increase throughput, because it could process incoming orders as fast as the client
submitted them. The problem in doing so is that it is possible for the calls to the
Order
, Shipment
,
and Billing
services to fail independently, and if they
do, all hell will break loose. For example, the Order
service might discover that there were no items in the inventory matching the customer
request, while the Shipment
service tried to ship the
nonexisting item and the Billing
service had already
billed the customer for it.
Using asynchronous calls on a set of interacting services requires that you change
your code and your workflow. As illustrated in Figure 8-5, to call the helper services
asynchronously, you need to string them together. The Store
service should call only the Order
service, which in turn should call the Shipment
service
only if the order processing was successful, to avoid the potential inconsistencies just
mentioned. Similarly, only in the case of successful shipment should the Shipment
service asynchronously call the Billing
service.
In general, if you have more than one service in your asynchronous workflow, you should have each service invoke the next one in the logical execution sequence. Needless to say, such a programming model introduces tight coupling between services (they have to know about each other) and changes to their interfaces (you have to pass in additional parameters, which are required for the desired invocation of services downstream).
The conclusion is that using asynchronous instead of synchronous invocation introduces major changes to the service interfaces and the client workflow. Asynchronous invocation on a service that was built for synchronous execution works only in isolated cases. When dealing with a set of interacting services, it is better to simply spin off a worker thread to call them and use the worker thread to provide asynchronous execution. This will preserve the service interfaces and the original client execution sequence.
3.144.113.55