Why Asynchronous Delegates Are Your Friend - ' Callback on Completion ' (
Page 4 of 4 )
Callback on Completion
Every delegate's BeginInvoke method has two extra parameters after any Invoke parameters: The first is an AsyncCallback delegate and the second is an arbitrary object.
If the AsyncCallback delegate is non-null, the asynchronous mechanism will Invoke it synchronously (in the same ThreadPool thread that runs the delegate; again, I talk about the system ThreadPool in the other half of this pair of articles) once the asynchronously-invoked delegate returns. An AsyncCallback delegate takes a single parameter, which is the same IAsyncResult that BeginInvoke returns and that you must pass to EndInvoke. The AsyncState property of the IAsyncResult that's passed to your AsyncCallback delegate contains the object parameter that you passed to BeginInvoke.
It took me a long time to see any sense in this callback scheme. Why two delegates? Why not just roll the second one into the first, maybe via a delegate to a method that calls the prequel and the sequel? I mean, yes, you can write a Reflection-based universal EndInvoke that will always call the right EndInvoke on the passed IAsyncResult:
public static class FireAndForget
{
public static void EndInvoke(IAsyncResult Async)
{
#if DEBUG
Delegate D = (Delegate)Async.AsyncState;
// raise an exception if AsyncState is not a delegate
#else
object D = Async.AsyncState;
#endif
Type TypeofDelegate = D.GetType();
System.Reflection.MethodInfo EndInvoke =
TypeofDelegate.GetMethod("EndInvoke", new Type[] { typeof(IAsyncResult) });
EndInvoke.Invoke(D, new object[] { Async });
}
}
You'd pass a delegate to this late-bound EndInvoke routine as the penultimate argument to BeginInvoke (the AsyncCallback delegate) and you'd pass the asynchronous delegate itself as the final argument:
FourStrings Background = new FourStrings(Quartet);
// FourStrings is a delegate type
Background.BeginInvoke("John", "Paul", "George", "Ringo",
FireAndForget.EndInvokeDelegate, Background);
But I don't think this fire and forget scenario is the real point of the callback mechanism. The real point is much more likely to be that you can break a long operation into a blocking fetcher and a post-block consumer, and both will run in the same ThreadPool thread. You certainly could use a second thread to block and resume, but that second thread is not free. The callback scheme runs both the agent that blocks and returns data and also the post-block processor in the same ThreadPool thread. ThreadPool threads are reusable, so often using a ThreadPool thread to run some code means that you can simply reuse an existing thread that's sitting idle, instead of having to create a new thread or two.
Perhaps a future C# will include a language feature that generates the BeginInvoke callback from, perhaps, a using statement:
using DelegateInvoke(DelegateParams)
delegate (IAsyncResult Result)
{
// post-block code.
// (using statement would automatically call EndInvoke,
// unless you did)
}
That would certainly make the callback feature a lot easier to use. In the meantime, I suggest you think of the callback as an advanced feature to explore when profiling reveals that you really do have too much handshaking going on. Until then, simply launching a thread by asynchronously invoking a delegate (doing as much other work as you can) and then blocking until it returns its result via EndInvoke, is not only the simplest way to pass multiple parameters to a thread, but also an efficient way to execute a method in a background thread.
New Hardware
These threading techniques are useful on single-processor machines to handle long running processes within GUI apps. On hyper-threaded and/or multi-core machines, free threading is the key to getting maximum performance. For example, many foreach loops can be easily rewritten to process each item in a new thread. It's easy to turn
foreach (T Datum in Data)
P(Datum); // Process each Datum in the current thread
into
List AsyncResults = new List();
// Process each Datum in its own thread
foreach (T Datum in Data)
AsyncResults.Add(P.BeginInvoke(Datum, null, null));
// Wait for all threads to finish
foreach (IAsyncResult Async in AsyncResults)
P.EndInvoke(Async);
Instead of calling P on each Datum in turn, processing one after another, the threaded version processes each simultaneously in its own thread and then blocks until all threads complete. This slows a uni-processor machine a little, but is about twice as fast on a hyper-threaded machine, or four times as fast on a dual-core, hyper-threaded machine. (Obviously, this is schematic code; real code would make some effort to match the number of threads to the number of processors.)
Jon Shemitz is a consultant and an author. You may contact him at www.midnightbeach.com. This article will appear, in different form, in his forthcoming book, .NET 2.0 For Delphi Programmers (APress, June 2006).