Introduction
This article describes how to implement a delegate which performs much better during BeginInvoke
/EndInvoke
operations, than the delegates with default implementation of BeginInvoke
/EndInvoke
injected by CLR. The idea is rather simple, no cheats with IL and function pointers.
Background
In .NET, there is 3 ways to execute an operation or a task asynchronously. The first approach consists in using of System.Threading.ThreadPool
, which suits the best in most cases. The second approach is Thread.Start
, which fits better than ThreadPool
, for the cases of long-running operations. And the last one is APM pattern (pair of methods BeginSomething
/EndSomething
) provided by all delegates and some major classes, like Stream
, WebRequest
, etc. APM pattern suits the best when you need to retrieve a result of an asynchronous operation.
Let's consider more closely delegates in .NET. Each delegate in .NET is decorated by C# compiler with such methods as BeginInvoke
and EndInvoke
. These methods at some degree are abstract
- C# compiler does not provide any implementation at compile time. Implementation is injected later by CLR. BeginInvoke
/EndInvoke
seems to be the perfect choice when we need to retrieve a result or an absence of a result (which is called an exception) of an asynchronous operation. But the "perfect" word here is valid only in the scope of logical design. When it comes to performance, be careful: "Avoid using BeginInvoke
/EndInvoke
methods as much as possible. The reason is both methods internally use remoting infrastructure." - this statement I've recently encountered in one of the Microsoft's books (I've rephrased it a little bit). Basically, I was interested in the fact, how much slower than ThreadPool
, for instance, are those two methods. Therefore I've implemented my own versions of BeginInvoke
/EndInvoke
.
Using the Code
The usage of my APM pattern implementation is exactly the same, as the default one. Here is an example:
var fastDel = new FastInvokeDelegate<int>(() => MyAsyncTask(100));
var asyncResult = fastDel.BeginInvokeFast(null, null);
int result = fastDel.EndInvokeFast(asyncResult);
Here MyAsyncOperation
is some "don't-matter" function which returns an integer. As I mentioned, the usage of BeginInvokeFast
/EndInvokeFast
is exactly the same as BeginInvoke
/EndInvoke
. FastInvokeDelegate
here is just a simple regular delegate decorated with extension methods BeginInvokeFast
/EndInvokeFast
. Looks a bit cumbersome. Unfortunately, C# compiler at the moment does not provide any better language constructs to make it more nice-looking.
Benchmarks
I've prepared a small peace of code for testing purposes:
static void Main(string[] args)
{
Func<int,int > del = (i) => 100 + i;
var fastDel = new FastInvokeDelegate<int >(() => del(100));
Stopwatch stopWatch = new Stopwatch();
var asyncResults = new List<iasyncresult >(10000);
stopWatch.Start();
for (int i = 0; i < 10000; i++)
{
asyncResults.Add(del.BeginInvoke(i, null, null));
}
stopWatch.Stop();
Console.WriteLine("Delegate.BeginInvoke(): {0}", stopWatch.ElapsedTicks);
Thread.Sleep(10000);
stopWatch.Reset();
stopWatch.Start();
for (int i = 0; i < 10000; i++)
{
del.EndInvoke(asyncResults[i]);
}
stopWatch.Stop();
Console.WriteLine("Delegate.EndInvoke(): {0}", stopWatch.ElapsedTicks);
asyncResults = new List<iasyncresult >(10000);
GC.Collect();
stopWatch.Reset();
stopWatch.Start();
for (int i = 0; i < 10000; i++)
{
asyncResults.Add(fastDel.BeginInvokeFast(null, null));
}
stopWatch.Stop();
Console.WriteLine("FastInvokeDelegate.BeginInvoke(): {0}", stopWatch.ElapsedTicks);
Thread.Sleep(10000);
stopWatch.Reset();
stopWatch.Start();
for (int i = 0; i < 10000; i++)
{
var res = fastDel.EndInvokeFast(asyncResults[i]);
}
stopWatch.Stop();
Console.WriteLine("FastInvokeDelegate.EndInvoke(): {0}", stopWatch.ElapsedTicks);
}
Here's the output:
Now you can see, why I do call my implementation "fast"...
Implementation
Everything is pretty simple. No cheats with IL, no function pointers. All I did is created a simple regular .NET delegate and decorated it with two extension methods - BeginInvokeFast
and EndEnvokeFast
. That's the story. The most interesting part here would be IAsyncResult
implementation which does not initialize ManualResetEvent
until it is needed.
public delegate T FastInvokeDelegate<t>();
public static class DelegateExtensions
{
public static IAsyncResult BeginInvokeFast<t>
(this FastInvokeDelegate<t> del, object state, AsyncCallback callback)
{
return new FastInvokeAsyncResult<t>(del, callback, state);
}
public static T EndInvokeFast<t>(this FastInvokeDelegate<t> del,
IAsyncResult asyncResult)
{
var result = asyncResult as FastInvokeAsyncResult<t>;
if (result == null)
throw new InvalidOperationException("Wrong async result");
return result.End();
}
private class FastInvokeAsyncResult<t> : IAsyncResult
{
private volatile int m_isCompleted;
private ManualResetEvent m_asyncWaitHandle;
private volatile int m_asyncWaitHandleNeeded = 0;
private readonly AsyncCallback m_callback;
private readonly object m_asyncState;
private Exception m_exception;
private T m_result;
public FastInvokeAsyncResult(FastInvokeDelegate<t> work,
AsyncCallback callback, object state)
{
m_callback = callback;
m_asyncState = state;
Run(work);
}
public bool IsCompleted
{
get { return (m_isCompleted == 1); }
}
public bool CompletedSynchronously
{
get { return false; }
}
public WaitHandle AsyncWaitHandle
{
get
{
if (m_asyncWaitHandleNeeded == 1)
{
return m_asyncWaitHandle;
}
m_asyncWaitHandleNeeded = 1;
m_asyncWaitHandle = new ManualResetEvent(m_isCompleted == 1);
return m_asyncWaitHandle;
}
}
public object AsyncState
{
get { return m_asyncState; }
}
private void Run(FastInvokeDelegate<t> work)
{
ThreadPool.QueueUserWorkItem(delegate
{
try
{
m_result = work();
}
catch (Exception e)
{
m_exception = e;
}
finally
{
m_isCompleted = 1;
if (m_asyncWaitHandleNeeded == 1)
{
m_asyncWaitHandle.Set();
}
if (m_callback != null)
m_callback(this);
}
});
}
public T End()
{
if (m_isCompleted == 0)
{
AsyncWaitHandle.WaitOne();
AsyncWaitHandle.Close();
}
if (m_exception != null)
throw m_exception;
return m_result;
}
}
}
Conclusion
Use cases for FastBeginInvoke
/FastEndInvoke
are exactly the same as for BeginInvoke
/EndInvoke
. The only difference is performance.
History
- 1st July, 2009: Initial post