Monday, October 18, 2004

AsyncHelper - FireAndForget and FireAsync

When you want to call a method asynchronously you can have it execute on the ThreadPool very easily by creating a delegate and calling BeginInvoke on it:


void LongRunningOperation(int arg1, float arg2, object arg3)
{
//...
}

delegate void LongRunningOperationHandler(int arg1, float arg2, object arg3)
void RunLongRunningOperationAsynchronously(int arg1, float arg2, object arg3)
{
LongRunningOperationHandler handler = new LongRunningOperationHandler(LongRunningOperation);
handler.BeginInvoke(arg1, arg2, arg3, null, null);
}

Only thing is that the documentation as of the 1.1 version of the framework says that we must call EndInvoke on a delegate once we have called BeginInvoke or our application may leak.

The compiler generates the arguments for BeginInvoke and EndInvoke. BeginInvoke starts with all the in going and reference arguments. The last two arguments are common to all BeginInvoke methods. The second to last argument is an AsyncCallback delegate and the last argument is an the AsyncState, an object that we will be able to get back in the callback. BeginInvoke returns an object implementing the IAsyncResult:


public interface IAsyncResult
{
// Properties
object AsyncState { get; }
WaitHandle AsyncWaitHandle { get; }
bool CompletedSynchronously { get; }
bool IsCompleted { get; }
}


This same object is passed to the AsyncCallback handler.

To call the EndInvoke in the callback function we need to get access to the original delegate. One way to do this is pass it in as the AsyncState argument



void RunLongRunningOperationAsynchronously(int arg1, float arg2, object arg3)
{
LongRunningOperationHandler handler = new LongRunningOperationHandler(LongRunningOperation);
handler.BeginInvoke(arg1, arg2, arg3, new AsyncCallback(LongRunningOperationDone), handler);
}

void LongRunningOperationDone(IAsyncResult result)
{
LongRunningOperationHandler handler = result.AsyncState as LongRunningOperationHandler;
if (handler != null)
handler.EndInvoke(result);
}


The actual object implementing IAsyncResult is a System.Runtime.Remoting.Messaging.AsyncResult. IAsyncResult does not contain a property to get the original delegate but the AsyncResult has the AsyncDelegate property which we can use to get the delegate.



void RunLongRunningOperationAsynchronously(int arg1, float arg2, object arg3)
{
LongRunningOperationHandler handler = new LongRunningOperationHandler(LongRunningOperation);
handler.BeginInvoke(arg1, arg2, arg3, new AsyncCallback(LongRunningOperationDone), null);
}

void LongRunningOperationDone(IAsyncResult result)
{
AsyncResult resultObject = result as AsyncResult;
if (resultObject != null)
{
LongRunningOperationHandler handler = resultObject.AsyncDelegate as LongRunningOperationHandler;
if (handler != null)
handler.EndInvoke(result);
}
}



In many (most) cases I'm not interested in retrieving any results from the method. I use a class based on
Mike Woodring's method to call AsyncHelper.FireAndForget so that I don't have to bother with the EndInvoke.



// Based on AsyncHelper by Mike Woodring of http://staff.develop.com/woodring
//
// Use FireAndForget to execute a method in the thread pool without having to
// call EndInvoke. Note that the delegate passed to FireAndForget can only have a
// single Target.
// AsyncHelper.FireAndForget(new MyHandler(MyMethod), methodArg1, methodArg2, etc);
//
// To Fire an Event asynchronously (with each of the targets being called on its own thread
// use FireAsync:
// AsyncHelper.FireAsync(MyEvent, eventArg1, eventArg2, ...);
// FireAsync is designed to be called on an event containing zero, one or more Targets.
// Your FireEvent method does not need to check for null FireAsync will do it correctly even in a
// multithreaded environment
#region Using Statements
using System;
using System.Reflection;
using System.Threading;
using System.ComponentModel;
#endregion

namespace DanGo.Utilities
{
public class AsyncHelper
{
#region Private Types
// Private class holds data for a delegate to be run on the thread pool
private class Target
{
#region Private Fields
private readonly Delegate TargetDelegate;
private readonly object[] Args;
#endregion

#region Constructor
/// <summary>
/// Creates a new <see cref="Target"/> instance this holds arguments and contains
/// the method ExecuteDelegate to be called on the threadpool.
/// </summary>
/// <param name="d">The users delegate to fire</param>
/// <param name="args">The users arguments to the delegate</param>
public Target( Delegate d, object[] args)
{
TargetDelegate = d;
Args = args;
}
#endregion

#region Invoker
/// <summary>
/// Executes the delegate by calling DynamicInvoke.
/// </summary>
/// <param name="o">This parameter is required by the threadpool but is unused.</param>
public void ExecuteDelegate( object o )
{
TargetDelegate.DynamicInvoke(Args);
}
#endregion
}
#endregion

#region Public Static Methods
/// <summary>
/// Fires the delegate without any need to call EndInvoke.
/// </summary>
/// <param name="d">Target Delegate - must contain only one Target method</param>
/// <param name="args">Users arguments.</param>
public static void FireAndForget( Delegate d, params object[] args)
{
Target target = new Target(d, args);
ThreadPool.QueueUserWorkItem(new WaitCallback(target.ExecuteDelegate));
}

/// <summary>
/// Fires each of the members in the delegate asynchronously. All the members
/// will be fired even if one of them fires an exception
/// </summary>
/// <param name="del">The delegate we want to fire</param>
/// <param name="args">Each of the args we want to fire.</param>
public static void FireAsync(Delegate del, params object[] args)
{
// copy the delegate to ensure that we can test for null in a thread
// safe manner.
Delegate temp = del;
if(temp != null)
{
Delegate[] delegates = temp.GetInvocationList();
foreach(Delegate receiver in delegates)
{
FireAndForget(receiver, args);
}
}
}
#endregion
}
}


FireAndForget will only manage a delegate with a single handler. Often I want to fire events asynchronously. I do this with the AsyncHelper.FireAsync


Calling AsyncHelper.FireAsync has a number of advantages. Normally when we fire an event we don't know how long the clients will take to execute so that they can delay our processing. In addition if one clients handler throws an exception the following handlers will not get called.

Give AsyncHelper.FireAndForget and AsyncHelper.FireAsync a try!

0 Comments:

Post a Comment

<< Home