FireAndForget Asynchronous Utility Class for SQL Server Inserts and Updates
By Peter Bromberg
I have used the FireAndForget pattern for a number of years; the first implementation I ever saw was by Mike Woodring. FireAndForget provides a threadsafe, non-blocking framework to invoke methods without having to wait for the method to return. In other words, it does not matter what your method does or how long it takes, once you hand it off to the FireAndForget utility method, your call returns immediately.
FireAndForget uses a DelegateWrapper which calls InvokeWrappedDelegate, which in
turn calls the DynamicInvoke method of the wrapped delegate. It executes the
specified delegate with the specified arguments asynchronously on a thread pool
thread. EndWrapperInvoke calls EndInvoke on the wrapper and Close on the resulting
WaitHandle to prevent resource leaks. In other words, FireAndForget makes an
asynchronous call but it handles the async callback internally for you, and you
don't have to wait.
NOTE that this is not the same as using the BeginExecuteNonQuery method of the SqlCommand
object; that requires an async callback method which must be invoked to complete the operation. With FireAndForget, everything is handled internally. Think
of it like a coin toll booth where you can just throw your change at it and never
even have to slow down.
When would you use this? Anytime you have a high volume of requests or threads for
which you need to "do something" very fast in a non-blocking way, such
as updating a counter column in your table, sending out a quick email, and so
on. You don't want your page request "hung up" with a blocking call
waiting for a method to return, and this is one of the best ways to do this.
Having said that, this is not a "Band Aid" for bad database design. If
there is something wrong with your database and you're getting timeouts because
of page locking, resource contention or deadlocks, this isn't going to help you
- you need to fix your schema, SQL and code first, get it to work fast and
correctly. Then FireAndForget will make it even faster.
Here is the simplified FireAndForget static ThreadUtil class:
using System;
using System.Collections.Specialized;
using System.Configuration;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Security;
using System.Text;
using System.Threading;
using PAB.Data.Utils;
namespace FireAndForget
{
/// <summary>
/// Provides threadsafe, non-blocking methods to invoke methods using the Fire and
Forget pattern
/// <b>Usage:</b>
/// <example>FireAndForget.ThreadUtil.InvokeSproc(connectionString,sprocName,parameters)</example>
/// </summary>
public static class ThreadUtil
{
#region Delegates
public delegate void InvokeSprocDelegate(string connectionString, string sprocName, object[] parameters);
#endregion
/// <summary>
/// Callback used to call <code>EndInvoke</code> on the asynchronously
/// invoked DelegateWrapper.
/// </summary>
private static AsyncCallback callback = EndWrapperInvoke;
/// <summary>
/// An instance of DelegateWrapper which calls InvokeWrappedDelegate,
/// which in turn calls the DynamicInvoke method of the wrapped
/// delegate.
/// </summary>
private static DelegateWrapper wrapperInstance = new DelegateWrapper(InvokeWrappedDelegate);
public static void InvokeSproc(string connectionString, string sprocName, object[] parameters)
{
FireAndForget(new InvokeSprocDelegate(InvokeASproc),
new object[] { connectionString, sprocName, parameters });
}
private static void InvokeASproc (string connectionString, string sprocName, object[] parameters)
{
SqlHelper.ExecuteNonQuery(connectionString, sprocName, parameters);
}
/// <summary>
/// Executes the specified delegate with the specified arguments
/// asynchronously on a thread pool thread.
/// </summary>
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);
}
/// <summary>
/// Invokes the wrapped delegate synchronously
/// </summary>
private static void InvokeWrappedDelegate(Delegate d, object[] args)
{
d.DynamicInvoke(args);
}
/// <summary>
/// Calls EndInvoke on the wrapper and Close on the resulting WaitHandle
/// to prevent resource leaks.
/// </summary>
private static void EndWrapperInvoke(IAsyncResult ar)
{
wrapperInstance.EndInvoke(ar);
ar.AsyncWaitHandle.Close();
}
#region Nested type: DelegateWrapper
/// <summary>
/// Delegate to wrap another delegate and its arguments
/// </summary>
private delegate void DelegateWrapper(Delegate d, object[] args);
#endregion
}
}
The basic pattern to "fill in" a custom FireAndForget method for whatever
it is you may wish to do is:
1) Declare a Delegate that exactly maches the signature of your proposed method,
whose return type is void.
2) Create a private static void method that accepts your required parameters, and
does your actual business logic.
3) Create a public static void method to accept your required parameters, and inside
this method, call
FireAndForget(new YourDelegate(YourPrivateMethod), new object[] { param1,param2,...
});
It is important to understand that such calls are are placed in the Threadpool queue
until they can be serviced as threads become available, so if your Threadpool
is very busy, your method call may not be executed immediately.
I've provided a simple Windows Form test app to go with this, and as long as you
have Northwind installed, it has a button that will add the required test sproc
of "InsertCategory" for you. This uses the DAAB SqlHelper class, so
all you need to do is pass in the connection string, name of the sproc, and an
object array of the parameter values in the sproc in the proper order. This has
the added benefit that it caches the SqlParameter collection and re-uses it on
subsequent calls, making it even faster.
You can download the Visual Studio 2010 Solution here.
Popularity (1328 Views)
 |
| Biography - Peter Bromberg |
Peter Bromberg is a C# MVP, MCP, and .NET expert who has worked in banking, financial and telephony for over 20 years. Pete focuses exclusively on the .NET Platform, and currently develops SOA and other .NET applications for a Fortune 500 clientele. Peter enjoys producing digital photo collage with Maya,playing jazz flute, the beach, and fine wines. You can view Peter's UnBlog and IttyUrl sites.
|  |
|
|
Article Discussion: FireAndForget Asynchronous Utility Class for SQL Server Inserts and Updates
Scott Gall replied
to Peter Bromberg at Tuesday, October 19, 2010 4:38 AM
Very interesting Article,
I have one question, if an error is raised from the public static void method could it be caught and handled in the method which called FireAndForget? or is this going to be out of scope at this point?
A Fire and Foreget approach can work exceptionally well if you are sure there will not be an unexpected error (though that's hard to guarentee) and I am curious how you would suggest dealing with such an occurance.
Thank you in advance for your time.
Peter Bromberg replied
to Scott Gall at Tuesday, October 19, 2010 4:38 AM
It would be out of scope. You could wrap the logic in EndWrapperInvoke in a try / catch block and log the exception.
Scott Gall replied
to Peter Bromberg at Tuesday, October 19, 2010 4:38 AM
Thanks for the quick responce, and a good idea.
Alex Joyce replied
to Peter Bromberg at Tuesday, October 19, 2010 4:38 AM
Hi Peter
I Really like this great post, I am hoping you can answer one
question for me, The SQL helper object is caching the parameters, but because it
is being called from a new thread each time, is it not going to go out of scope
and get picked up by the GC after each thread completes, hence when the next new
thread executes it will always have to go to the db for the parameters as it
will always have a new empty cache? I ask the question because I am thinking
about using the SQL helper class in an async logging scenario I am working on
and I wonder if I should use the explicit sql parameter array on
ExecuetnNonQuery instead of the object array to save an extra round trip for
picking up the parameters each time
Thanks in advance
Peter Bromberg replied
to Alex Joyce at Tuesday, October 19, 2010 4:38 AM
The SqlHelper class caches only the parameters, not the parameter values. So each time it is called with the same stored proc name and connection string, it supplies your new values. This is threadsafe behavior.