Hi, today I would like to show you Async Pattern for .NET 1.1 and C#. I prepared a training workshop on this subject, and this is one of my excellent examples. So, below I will show you an example test code of the entire async versus sync performance measurement solution. You may wonder how that is possible in .NET 1.1 without the Task Parallel Library and the async keyword? The answer is straightforward. Everything you need is a delegate. If I asked what the biggest success of .NET 1.1 was, I would say it was a delegate keyword in C#. So let me show you a very cool pattern for creating Async invocation without Tasks in .NET 1.1. That is light and can be implemented in every .NET version above 1.1. The picture on the right with four CPUs shows this pattern has outstanding scalability and a muted impact on systems. And as with every good pattern it can be very easy to understand.
namespace AsyncInvocationTest { using System; using System.Diagnostics; using System.Globalization; using System.Runtime.CompilerServices; using System.Threading; class InvocationTest { int status; int maxThreads; public InvocationTest(int status, int maxThreads) { this.status = status; this.maxThreads = maxThreads; } [MethodImpl(MethodImplOptions.Synchronized)] private int Increment(int arg) { lock (this) { status += arg; int newStatus = status; return newStatus; } } private int Invocation(int arg) { // Slow part - internal slow calculation. CpuTimeConsumption(); // Fast part - synchronized for sure. return Increment(arg); } #region Helpful Property and Method public int Status { get { return status; } } private void CpuTimeConsumption() { for (int i = 0; i < int.MaxValue / 400; i++) ; } #endregion #region Classic Sync Invocation public int[] Run(int count) { int[] result = new int[count]; for (int i = 0; i < count; i++) { int arg = i + 1; result[i] = Invocation(arg); } return result; } #endregion #region Powerful Async Invocation Pattern private delegate int InvocationDelegate(int arg); public int[] RunAsync(int count) { int[] result = new int[count]; InvocationDelegate invoker = new InvocationDelegate(Invocation); IAsyncResult[] results = new IAsyncResult[count]; WaitHandle[] waits = new WaitHandle[maxThreads]; for (int i = 0; i < count; ) { for (int t = 0; t < maxThreads && i < count; t++, i++) { int arg = i + 1; IAsyncResult asyncResult = invoker.BeginInvoke(arg, null, null); results[i] = asyncResult; waits[t] = asyncResult.AsyncWaitHandle; } WaitHandle.WaitAll(waits); } for (int i = 0; i < count; i++) { try { result[i] = invoker.EndInvoke(results[i]); } catch { result[i] = default(int); } } return result; } #endregion } class Program { #region Invocation Performance Test enum TestKind { Sync = 0, Async = 1 } static int[] TestInvoker(TestKind kind, int startStatus, int count, int concurrentThreads /*max == 64*/) { InvocationTest invoker = new InvocationTest(startStatus, concurrentThreads); int[] result = null; Stopwatch st = Stopwatch.StartNew(); if (kind == TestKind.Async) result = invoker.RunAsync(count); else result = invoker.Run(count); st.Stop(); Console.WriteLine( "Invoker end status {0}, {1:0000} ms {2:00} threads, invocation {3}.", invoker.Status, st.Elapsed.TotalMilliseconds, concurrentThreads, Enum.GetName(typeof(TestKind), kind).ToLower()); return result; } static void CheckResults(int[] rSync, int[] rAsync) { if (rSync.Length != rAsync.Length) { Console.WriteLine("Error in results."); return; } else { int[] rSyncSorted = new int[rSync.Length]; int[] rAsyncSorted = new int[rAsync.Length]; Array.Copy(rSync, rSyncSorted, rSync.Length); Array.Copy(rAsync, rAsyncSorted, rAsync.Length); Array.Sort(rSyncSorted); Array.Sort(rAsyncSorted); int[] rSyncChanges = new int[rSync.Length]; int[] rAsyncChanges = new int[rAsync.Length]; for (int i = rSyncSorted.Length - 1; i > 0; i--) { rSyncChanges[i] = rSyncSorted[i] - rSyncSorted[i-1]; rAsyncChanges[i] = rAsyncSorted[i] - rAsyncSorted[i-1]; } rSyncChanges[0] = rSyncSorted[0]; rAsyncChanges[0] = rAsyncSorted[0]; Array.Sort(rSyncChanges); Array.Sort(rAsyncChanges); int sumSync = 0; int sumAsync = 0; for (int i = 0; i < rSyncChanges.Length; i++) { sumSync += rSyncChanges[i]; sumAsync += rAsyncChanges[i]; } if (sumSync != sumAsync) { Console.WriteLine("ERROR!"); return; } } } #endregion static void Main() { Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US"); for (int i = 0; i < 10; i++) { int[] r1 = TestInvoker(TestKind.Sync, 1, 128, 1); int[] r2 = TestInvoker(TestKind.Async, 1, 128, (int)((i + 1) * 6.4)); CheckResults(r1, r2); } Console.WriteLine("Press any key to continue..."); Console.ReadKey(); } } }
And here is a result of invoke this performance test on the slow PC but with 4 logical CPU.
Invoker end status 8257, 1397 ms 01 threads, invocation sync. Invoker end status 8257, 0660 ms 12 threads, invocation async. Invoker end status 8257, 1153 ms 01 threads, invocation sync. Invoker end status 8257, 0571 ms 19 threads, invocation async. Invoker end status 8257, 1088 ms 01 threads, invocation sync. Invoker end status 8257, 0563 ms 25 threads, invocation async. Invoker end status 8257, 1118 ms 01 threads, invocation sync. Invoker end status 8257, 0561 ms 32 threads, invocation async. Invoker end status 8257, 1056 ms 01 threads, invocation sync. Invoker end status 8257, 0531 ms 38 threads, invocation async. Invoker end status 8257, 1052 ms 01 threads, invocation sync. Invoker end status 8257, 0531 ms 44 threads, invocation async. Invoker end status 8257, 1061 ms 01 threads, invocation sync. Invoker end status 8257, 0528 ms 51 threads, invocation async. Invoker end status 8257, 1049 ms 01 threads, invocation sync. Invoker end status 8257, 0538 ms 57 threads, invocation async. Invoker end status 8257, 1056 ms 01 threads, invocation sync. Invoker end status 8257, 0532 ms 64 threads, invocation async. Press any key to continue...
Ok, so what is the async pattern? You can find it in the region I named Powerful Async Invocation Pattern :). My first conclusion is that you have clean code and excellent scalability when you use the delegate BeginInvoke and EndInvoke methods. As you can see, results are 2 times faster when I use this pattern because I have two physical CPUs, not four (two of them are disabled). So, scalability is outstanding. We improved invocation time about half time faster. And the second conclusion of this pattern is that it is straightforward to understand. The third conclusion is about using concurrent threads. As you can see, the fastest time of invocation was when we used 51 concurrent threads for this test solution, but in every test, the invocation time was similar. And last, I have the fourth conclusion: when you must use .NET 1.1, you can design and develop solutions without TPL and Async. This is the power of the delegate object. And you probably see right now how easy you can connect with web service many times async? Everything you need is calling Web Service client method in the method you use by the delegate, then Powerful Async Invocation Pattern gives you all you need :).
Regards,
P ;).
Pingback: The Powerful Async Pattern in .NET 2.0 with Threads « .NET Rules! Blog
Pingback: The Powerful Async Pattern in .NET 4.0 with Tasks « .NET Rules! Blog
Pingback: The Powerful Async Pattern in .NET 4.0 with Parallel « .NET Rules! Blog
Pingback: The Powerful Async Pattern in .NET and C# 5.0 with Async CTP1 « .NET Rules! Blog