Asynchronous Fire And Forget Redux

by Peter A. Bromberg, Ph.D.

Peter Bromberg
"Explicit code is easier to understand, which makes the code easier to modify" -- Martin Fowler

Some time ago I wrote up a piece about using Mike Woodring's "AsyncHelper" class that implements his version of a custom Fire And Forget Delegate invocation pattern that eliminates (at least, usually so) the potential for leaks (e.g, "Handles Gone Wild 2005") when using BeginInvoke without taking care of the resultant callback.



When you provide "FireAndForget" with a delegate to execute, it invokes it's own internal delegate asynchronously,
and that delegate executes the one you provided synchronously to it. This gives the effective result of having the delegate you provided being executed asynchronously, but it allows the helper class to take care of the dirty-work of calling EndInvoke on the delegate for you, so that you don't have to handle it. This prevents the WaitHandle from leaking until garbage collection. In situations where BeginInvoke would be called many times in quick succession, without handling EndInvoke, you could end up with a huge number of handles wating around until the garbage collector started finalizing them. Performance wise, you shouldn't leave things to be finalized when it can be avoided, as Jon Skeet points out so astutely in parts of his excellent threading tutorial. Jon also provides an implementation of this that he calls "ThreadUtil", which you can see below.

Since I keep returning to this pattern in parts of my work where I need to be able to queue a WorkItem for a method call in rapid-fire succession, but don't have the luxury of "waiting around" to handle the callback, I thought it would be appropriate to wire up a new example that makes say, 100,000 Sql Server insert calls using this pattern, and set it up in such a way that you can actually see how long it takes, what is the average time to queue each item, and then wait for all the inserts to complete and see the time difference between how long it takes to queue all the WorkItems, and how long it takes for them all to complete, even though your code has been free to do other things in the interim. It's a simple Console App, but for those who need a refresher on this useful technique, it should be helpful. I've kept it in .NET 1.1, but it works in .NET 2.0 exactly the same way.

Different ways methods can be called

Before we revisit the FireAndForget pattern, a quick review of the ways that a method can be called is in order. They are:

  • Direct Method Call (Synchronous)
  • DelegateInstance.method.Invoke (Synchronous)
  • DelegateInstance.BeginInvoke (Asynchronous)
  • Delegate.DynamicInvoke (Synchronous)

Delegate.Method.Invoke:

Invoke is similar to direct calling of methods. The difference is that when invoke is used, the execution of the method is done synchronously but on a different thread. Internally, Invoke calls BeginInvoke and EndInvoke synchronously.

Control.Invoke:

This executes the specified delegate on the UI thread. This can be used to divert the call back from Async calls.

BeginInvoke and EndInvoke:

Asynchronous way of invoking a method call. Execution happens on a different thread.

DynamicInvoke:

The difference between Invoke and DynamicInvoke is that Invoke requires the target object that instantiated the method as a parameter to execute the method, whereas DynamicInvoke doesn’t require the target object; it calls the method dynamically with the parameter list.

If the method is static then null can be passed as target object in Invoke during which Invoke is similar to DynamicInvoke.

Invoke

Delegate del = new Delegate (this. Method1);

del.Method.Invoke (targetobject, new object [] {“params”});

If the method is static then:

del.Method.Invoke (null, new object [] {“params”});

DynamicInvoke

del.DynamicInvoke (new object [] params);

The output returned from BeginInvoke method call is the IAsyncResult, and the output returned from EndInvoke is the return type of the delegate signature.

FireAndForget Redux:

First, the ThreadUtil Class if you have never seen it before. Read the code comments, and notice that the EndWrapperInvoke method performs the EndInvoke method internally and then CLOSES the WaitHandle:

 using System;
using System.Threading;

public class ThreadUtil
{    
	/// 	
	/// Delegate to wrap another delegate and its arguments
	/// 
	delegate void DelegateWrapper (Delegate d, object[] args);

	/// 
	/// An instance of DelegateWrapper which calls InvokeWrappedDelegate,
	/// which in turn calls the DynamicInvoke method of the wrapped
	/// delegate.
	/// 
	static DelegateWrapper wrapperInstance = new DelegateWrapper (InvokeWrappedDelegate);
    
	/// 
	/// Callback used to call EndInvoke on the asynchronously
	/// invoked DelegateWrapper.
	/// 
	static AsyncCallback callback = new AsyncCallback(EndWrapperInvoke);

	/// 
	/// Executes the specified delegate with the specified arguments
	/// asynchronously on a thread pool thread.
	/// 
	public static void FireAndForget (Delegate d, params object[] args)
	{
		// Invoke the wrapper asynchronously, which will then
		// execute the wrapped delegate synchronously (in the
		// thread pool thread)
		wrapperInstance.BeginInvoke(d, args, callback, null);
	}

	/// 
	/// Invokes the wrapped delegate synchronously
	/// 
	static void InvokeWrappedDelegate (Delegate d, object[] args)
	{
		d.DynamicInvoke(args);
	}

	/// 
	/// Calls EndInvoke on the wrapper and Close on the resulting WaitHandle
	/// to prevent resource leaks.
	/// 
	static void EndWrapperInvoke (IAsyncResult ar)
	{
		wrapperInstance.EndInvoke(ar);
		ar.AsyncWaitHandle.Close();
	}
}

   

And now, my little "excersizer" test Console class:

using System;
using System.Data;
using System.Data.SqlClient;

namespace FireAndForgetUtil
{
	class FireForgetTester
	{
		static string connectionString = "server=(local);database=test;uid=sa;pwd=;";

	/STEP ONE: Create a delegate whose signature matches the method to be called
		delegate void InsertDelegate(string first, string second, int num);
	// store our test iterations number
        static int numIterations = 100000;
        

		[STAThread]
		static void Main(string[] args)
		{
			
		DateTime start = DateTime.Now;
		DateTime done;
      //STEP TWO: Create a new instance of your delegate, pointing to your target method
			Delegate dWrite = new InsertDelegate(WriteIt);
		
		for(int i=0;i<numIterations;i++)
		{
		///STEP THREE: Execute FireAndForget helper method,passing the Delegate instance,
	// and then passing the method parameters as an object array
			ThreadUtil.FireAndForget(dWrite,new object[]{"hoohoo", "hahaa",  i });
			}
               
           done = DateTime.Now;
			TimeSpan elapsed =done-start;
			double totTime =elapsed.TotalMilliseconds;
			double avgTime = totTime / numIterations;
			Console.WriteLine(elapsed.TotalMilliseconds.ToString() );
			Console.WriteLine("Average time to Queue each WorkItem : " +avgTime.ToString());
			Console.WriteLine("Wait for \"DONE.\",then Press any key to quit.");
            Console.ReadLine();				
		}


		static void WriteIt(string first, string second, int num)
		{

			SqlConnection cn =new SqlConnection(connectionString);
			SqlCommand cmd = new SqlCommand();
			cmd.Connection =cn;
			cmd.CommandText ="dbo.InsertTblTest";
			cmd.CommandType=CommandType.StoredProcedure ;
			cmd.Parameters.Add(new SqlParameter("@First", first));
			cmd.Parameters.Add(new SqlParameter("@Second", second));
			try
			{
				cn.Open();
				cmd.ExecuteNonQuery();
			}
			catch (Exception ex)
			{
				Console.WriteLine(ex.Message);
			}
			finally
			{

			cn.Close();
			cmd.Dispose();
			}
			if(num==numIterations-1) Console.WriteLine("DONE."); 			
		}
	}
}

So here is what happens:

First, at class level, we declare our Delegate with the signature of the WriteIt method (void, two strings and an int for parameters).

Next, we create an instance of our Delegate pointing to the WriteIt method.

Next, we begin our iteration loop -- in this case 100,000 of them. In this loop, we execute the ThreadUtil.FireAndForget method, passing in the target method and an object array of the parameters for this particular call.

Finally we capture the elapsed time to do all 100,000 iterations, and compute the average time for each enqueue of a WorkItem to the FireAndForget method.

In the bottom of the WriteIt method, we check to see when the last insert was actually completed and we write out "DONE". Since we have a Console.ReadLine at the end of the Main Method, it will block until we press the enter key.

You can use this pattern for virtually any method call that "Does Something" reliable, where it is not particularly necessary for you to receive or wait around for a return object. Syslog messaging, database inserts, UDP Messages, MSMQ Messages, you name it. All you have to do is ensure your delegate matches the method signature, and use the pattern as illustrated above.

One caveat: It is always a good idea to put any code that could possibly throw an exception inside a try / catch / finally pattern. If you are firing off large numbers of calls where connections or other resources that need to be disposed are involved, and you don't do this properly, then in using the FireAndForget pattern you are going to have a real mess on your hands. You've been warned!

If you have an operation say, in logging queries, or call detail records, or anything similar where you may get large numbers of these that must be processed at lightning speed on a non-blocking call, this pattern can prove immensely useful to you.

The Bottom Line

On a typical 32-bit machine you are likely to see something like this:

5687.5728 <-- that's 5.6 seconds for 100,000 invocations
Average time to Queue each WorkItem : 0.056875728

The "DONE" Message may appear some 20 or even 30 or more seconds later. On a 64-bit machine with more RAM, you may see times roughly half of the above. On the tests I ran, I have never seen an insert fail. Obviously, there can be some significant advantages to using this pattern. In the download below, there is a SqlScript folder containing the script you can run in Query Analyzer to set up the TEST Database and table, and the stored proc for the inserts. I suggest that if you are running various tests, issue a "Truncate table tblTest" statement before each one so that you wll level the playing field before your next test. Don't forget to make sure the connection string, which is at the top of the Console App class, is correct.

Download the Visual Studio 2005 solution that acommpanies this article


Peter Bromberg is a C# MVP, MCP, and .NET consultant who has worked in the banking and financial industry for 20 years. He has architected and developed web - based corporate distributed application solutions since 1995, and focuses exclusively on the .NET Platform.
Article Discussion: