Using Delegates Asynchronously
by Richard Blewett02/24/2003
In the article "
Understanding the Nuances of Delegates in C#," by Satya Komatineni,
the concept of delegates and their general pattern of usage in C# was introduced.
This article takes the subject further and looks at another feature of delegates--the built-in ability to perform tasks asynchronously by handing off work to
the system thread pool. To understand exactly how delegates
perform their async magic, an understanding of how the system thread
pool works is first required. For each process in which it is loaded, the CLR manages
a thread pool. This thread pool consists of a queue to which pieces of work
are added, and a pool of threads that service the queue (by dequeuing the pieces
of work). The thread pool is exposed to the world via the System.Threading.ThreadPool
class.
The number of threads in the pool is not fixed but gradually grows and shrinks,
according to certain heuristics. Initially at 0, the pool grows as the number of concurrent,
outstanding pieces of work grows. As threads stand idle, they
may be removed from the pool. The pool is capped at 25 threads and this figure
is not alterable from managed code. Applications hosting the CLR can change it
via the .NET unmanaged API. For example, the maxWorkerThreads attribute
in the processModel element in machine.config specifies the
maximum size of the thread pool in the ASP.NET worker process.
Thread pool threads have a number of specific characteristics:
- They are background threads--in other words, they will not prevent your process terminating, should every non-background thread in your process terminate.
- Their priority should not be altered. The thread is "owned" by the thread pool and may be used for a variety of tasks--for example, remoted calls are serviced on thread pool threads. Altering the priority of a thread pool thread will cause other tasks to be bound by the new priority.
-
The COM Apartment affinity (exposed as the
ApartmentStateproperty of a thread) should not be altered for the same reason as the priority. - Long-running tasks should not be assigned to the thread pool, as this will decrease the number of available threads with which to service other requests.
- They should not be suspended or aborted. They are a system, not application, resource.
System.Threading.Thread class. However, the thread pool itself
is not the focus of this article; rather, how delegates can be leveraged to have
work processed by the thread pool is.
Delegates Under the Covers
So what happens when a delegate type if declared? Consider the following delegate declaration.
Code Example 1
delegate long CalcNthPrimeNumber( int N );
This can be used for invoking methods that calculate a specific prime number (this can be a very lengthy calculation): The C# compiler generates code equivalent to the following:
Code Example 2
class CalcNthPrimeNumber : System.MulticastDelegate
{
// ctor omitted
// Used in synchronous execution
public long Invoke( int N );
// Async execution methods
public IAsyncResult BeginInvoke( int N,
AsyncCallback callback,
object asyncState );
public long EndInvoke( IAsyncResult ar );
}
Invoke is the method that the compiler calls during standard
synchronous execution. However, the interesting methods for this discussion are
BeginInvoke and EndInvoke. BeginInvoke will
always return an IAsyncResult and will always have its last two
parameters as an AsyncCallback delegate and a System.Object.
EndInvoke will always have as its last parameter an IAsyncResult.
The rest of the signatures of these two methods are dependent on the signature
of the delegate in question. BeginInvoke always takes any
parameters passed by value (unadorned) or reference (ref) before
the callback delegate. EndInvoke always has the same return type
as the delegate signature, and also has any ref or out
parameters before the IAsyncResult.
Invoking Delegates Asynchronously
There are four patterns in async delegate execution: Polling, Waiting for
Completion, Completion Notification, and "Fire and Forget". Before explaining the
patterns, however, there is one caveat about async delegate execution. You
cannot execute a multicast delegate (one that calls more than one client)
asynchronously. Each client must be called asynchronously in turn by processing
the stored list of clients, the Invocation List. Failing to do this
results in an ArgumentException being thrown. Processing the
Invocation List turns out to be a far from onerous task--below is example
code:
Code Example 3
CalcNthPrimeNumber c;
// Code omitted when numerous clients
// are hooked up to the delegate
if( c != null )
{
foreach( CalcNthPrimeNumber cpn in c.GetInvocationList())
{
// Now call each cpn asynchronously
}
}
Spawning a method onto a worker thread is achieved by calling BeginInvoke
on a delegate that wraps the method. This tells the delegate to queue the
method invocation onto the Thread Pool. BeginInvoke takes all
unadorned and ref parameters, as well as an optional AsyncCallback
and System.Object (both of which will be explained later).
Once a method has been given to a worker thread, the thread that instigated the async call carries on with its own work. Given this, how does it find out what the async method returned?
Harvesting Results
Retrieving results is a simple operation; all that is required is a call to EndInvoke.
However, there is an issue: if the async method has not completed, EndInvoke
will block until it completes. This may be the required behavior--that the
caller gets on with other work and then calls EndInvoke when the
results are needed. However, what if the caller can usefully get on with work
until the async call has completed? Calling EndInvoke before the
call is complete means the thread is blocked where it could still be carrying
out useful work. Also, what if the caller wanted to be able to timeout the wait
after a specified period, that the background task had been too lengthy? EndInvoke
blocks until the async method completes--an indeterminate point in time.
We saw in Code Example 2 that BeginInvoke returns an IAsyncResult
reference. The semantics of EndInvoke require that it must be
passed an IAsyncResult reference that matches that returned by the
corresponding BeginInvoke. This is because the asynchronous call
is represented by a call object that the IAsyncResult references.
Polling
To find out whether a call has completed, we can talk to the call object
representing the async call. Let's look at the definition of IAsyncResult:
Code Example 4
public interface IAsyncResult
{
object AsyncState{ get; }
WaitHandle AsyncWaitHandle { get; }
bool CompletedSynchronously { get; }
bool IsCompleted { get; }
}
As can be seen, one of the members of IAsyncResult has been
highlighted. IsCompleted returns true when the asynchronous is
complete; until that time it returns false. IsCompleted can,
therefore, be used to assess whether the long-running calculation is finished.
In Code Sample 5, the 672nd prime number is being requested and the calculation
will be performed on a thread pool thread. The main thread then polls until the
calculation is complete.
Code Example 5
void SpawnPrimeNumberCalc(int N)
{
CalcNthPrimeNumber cpn =
new CalcNthPrimeNumber(CalculatePi);
IAsyncResult ar = cpn.BeginInvoke( 672,
null,
null );
// Do some stuff
while( !ar.IsCompleted )
{
// Do some stuff
}
// we now know that the call is
// complete as IsCompleted has
// returned true
long primeNumber = cpn.EndInvoke(ar);
}
void CalculatePi(int n)
{
// calculate the prime number specified by n
}
Waiting with Timeout
As stated before, EndInvoke is a blocking operation. The calling
thread will not continue until the async call has completed. However, what if
instead of calculating the Nth prime number locally, the target method were to
invoke a web service to perform the calculation? This could involve a network
hop (which could have timeouts in the order of minutes, in the case of network
failure). It may be preferable to be able to enforce a more user-friendly
timeout, say, twenty seconds. Through the use of timers and IAsyncResult.IsCompleted
this could be achieved, but it is not necessarily the most efficient way of
doing it. Let's have another look at IAsyncResult:
Code Example 6
public interface IAsyncResult
{
object AsyncState{ get; }
WaitHandle AsyncWaitHandle { get; }
bool CompletedSynchronously { get; }
bool IsCompleted { get; }
}
This time another of the properties of IAsyncResult has been
highlighted: AsyncWaitHandle. This property returns a WaitHandle--an object that can be waited on using the WaitHandle.WaitOne method,
which takes a timeout as a parameter. It is, therefore, possible to spawn the
async call on to another thread and then wait for it to complete--but then
timeout the call if it carries on too long. The code in Code Sample 7
demonstrates this pattern:
Code Example 7
void SpawnPrimeNumberCalc(int N)
{
CalcNthPrimeNumber cpn = new CalcNthPrimeNumber(CalculatePi);
IAsyncResult ar = cpn.BeginInvoke( 672, null, null );
// Do some stuff
if( ar.AsyncWaitHandle.WaitOne( 20000, true ))
{
long primeNumber = cpn.EndInvoke(ar);
Console.WriteLine( "672nd Prime Number is: {0}", primeNumber );
}
else
{
Console.WriteLine( "Timed out calculating prime number");
}
}
void CalculatePi(int n)
{
// calculate the prime number specified by using
// a web service
}
The highlighted code waits for 20 seconds for the method to return; if it doesn't return in that time, a message to that effect is printed out to the console window. The final facility allows for the calling thread to be completed disengaged from collecting the values returned from an async call. Instead, we can request to be notified when the call completes.
Pages: 1, 2 |

