Use Task.Run instead of Delegate.BeginInvoke

I recently upgraded my projects to ASP.NET 4.5, and I have been waiting for the use of asynchronous features 4.5. After reading the documentation, I'm not sure if I can improve my code at all.

I want to complete the task asynchronously, and then forget about it. The way I'm currently doing this is to create delegates and then use BeginInvoke .

Here, one of the filters in my project creates an audit in our database every time a user accesses a resource that needs to be checked:

 public override void OnActionExecuting(ActionExecutingContext filterContext) { var request = filterContext.HttpContext.Request; var id = WebSecurity.CurrentUserId; var invoker = new MethodInvoker(delegate { var audit = new Audit { Id = Guid.NewGuid(), IPAddress = request.UserHostAddress, UserId = id, Resource = request.RawUrl, Timestamp = DateTime.UtcNow }; var database = (new NinjectBinder()).Kernel.Get<IDatabaseWorker>(); database.Audits.InsertOrUpdate(audit); database.Save(); }); invoker.BeginInvoke(StopAsynchronousMethod, invoker); base.OnActionExecuting(filterContext); } 

But in order to finish this asynchronous task, I need to always define a callback that looks like this:

 public void StopAsynchronousMethod(IAsyncResult result) { var state = (MethodInvoker)result.AsyncState; try { state.EndInvoke(result); } catch (Exception e) { var username = WebSecurity.CurrentUserName; Debugging.DispatchExceptionEmail(e, username); } } 

I would prefer not to use the callback at all because I don't need the result from the task that I call asynchronously.

How can I improve this code with Task.Run() (or async and await )?

+6
source share
3 answers

If I understand your requirements correctly, you want to start the task, and then forget about it. When the task completes, and if an exception occurs, you want to register it.

I would use Task.Run to create the task, and then ContinueWith to attach the continue task. This continuation task will log any exception that was thrown from the parent task. Also, use TaskContinuationOptions.OnlyOnFaulted to ensure that continuation is only performed if an exception occurs.

 Task.Run(() => { var audit = new Audit { Id = Guid.NewGuid(), IPAddress = request.UserHostAddress, UserId = id, Resource = request.RawUrl, Timestamp = DateTime.UtcNow }; var database = (new NinjectBinder()).Kernel.Get<IDatabaseWorker>(); database.Audits.InsertOrUpdate(audit); database.Save(); }).ContinueWith(task => { task.Exception.Handle(ex => { var username = WebSecurity.CurrentUserName; Debugging.DispatchExceptionEmail(ex, username); }); }, TaskContinuationOptions.OnlyOnFaulted); 

As a side note, background tasks and fire and swell scenarios are highly discouraged in ASP.NET. See Dangers of Implementing Duplicate Background Tasks in ASP.NET

+9
source

This may seem a bit inoperable, but if you just want to forget after it starts, why not use ThreadPool directly?

Sort of:

 ThreadPool.QueueUserWorkItem( x => { try { // Do something ... } catch (Exception e) { // Log something ... } }); 

I had to do some performance benchmarking for various methods of calling asynchronous calls, and I found that (not surprisingly) ThreadPool works much better, but also that BeginInvoke is actually not so bad (I'm on .NET 4.5). This is what I found out with the code at the end of the post. I did not find something like this on the Internet, so I took the time to check it out myself. Each call is not entirely accurate, but it is more or less functionally equivalent in terms of what it does:

  • ThreadPool : 70.80ms
  • Task : 90.88ms
  • BeginInvoke : 121.88ms
  • Thread : 4657.52ms

     public class Program { public delegate void ThisDoesSomething(); // Perform a very simple operation to see the overhead of // different async calls types. public static void Main(string[] args) { const int repetitions = 25; const int calls = 1000; var results = new List<Tuple<string, double>>(); Console.WriteLine( "{0} parallel calls, {1} repetitions for better statistics\n", calls, repetitions); // Threads Console.Write("Running Threads"); results.Add(new Tuple<string, double>("Threads", RunOnThreads(repetitions, calls))); Console.WriteLine(); // BeginInvoke Console.Write("Running BeginInvoke"); results.Add(new Tuple<string, double>("BeginInvoke", RunOnBeginInvoke(repetitions, calls))); Console.WriteLine(); // Tasks Console.Write("Running Tasks"); results.Add(new Tuple<string, double>("Tasks", RunOnTasks(repetitions, calls))); Console.WriteLine(); // Thread Pool Console.Write("Running Thread pool"); results.Add(new Tuple<string, double>("ThreadPool", RunOnThreadPool(repetitions, calls))); Console.WriteLine(); Console.WriteLine(); // Show results results = results.OrderBy(rs => rs.Item2).ToList(); foreach (var result in results) { Console.WriteLine( "{0}: Done in {1}ms avg", result.Item1, (result.Item2 / repetitions).ToString("0.00")); } Console.WriteLine("Press a key to exit"); Console.ReadKey(); } /// <summary> /// The do stuff. /// </summary> public static void DoStuff() { Console.Write("*"); } public static double RunOnThreads(int repetitions, int calls) { var totalMs = 0.0; for (var j = 0; j < repetitions; j++) { Console.Write("."); var toProcess = calls; var stopwatch = new Stopwatch(); var resetEvent = new ManualResetEvent(false); var threadList = new List<Thread>(); for (var i = 0; i < calls; i++) { threadList.Add(new Thread(() => { // Do something DoStuff(); // Safely decrement the counter if (Interlocked.Decrement(ref toProcess) == 0) { resetEvent.Set(); } })); } stopwatch.Start(); foreach (var thread in threadList) { thread.Start(); } resetEvent.WaitOne(); stopwatch.Stop(); totalMs += stopwatch.ElapsedMilliseconds; } return totalMs; } public static double RunOnThreadPool(int repetitions, int calls) { var totalMs = 0.0; for (var j = 0; j < repetitions; j++) { Console.Write("."); var toProcess = calls; var resetEvent = new ManualResetEvent(false); var stopwatch = new Stopwatch(); var list = new List<int>(); for (var i = 0; i < calls; i++) { list.Add(i); } stopwatch.Start(); for (var i = 0; i < calls; i++) { ThreadPool.QueueUserWorkItem( x => { // Do something DoStuff(); // Safely decrement the counter if (Interlocked.Decrement(ref toProcess) == 0) { resetEvent.Set(); } }, list[i]); } resetEvent.WaitOne(); stopwatch.Stop(); totalMs += stopwatch.ElapsedMilliseconds; } return totalMs; } public static double RunOnBeginInvoke(int repetitions, int calls) { var totalMs = 0.0; for (var j = 0; j < repetitions; j++) { Console.Write("."); var beginInvokeStopwatch = new Stopwatch(); var delegateList = new List<ThisDoesSomething>(); var resultsList = new List<IAsyncResult>(); for (var i = 0; i < calls; i++) { delegateList.Add(DoStuff); } beginInvokeStopwatch.Start(); foreach (var delegateToCall in delegateList) { resultsList.Add(delegateToCall.BeginInvoke(null, null)); } // We lose a bit of accuracy, but if the loop is big enough, // it should not really matter while (resultsList.Any(rs => !rs.IsCompleted)) { Thread.Sleep(10); } beginInvokeStopwatch.Stop(); totalMs += beginInvokeStopwatch.ElapsedMilliseconds; } return totalMs; } public static double RunOnTasks(int repetitions, int calls) { var totalMs = 0.0; for (var j = 0; j < repetitions; j++) { Console.Write("."); var resultsList = new List<Task>(); var stopwatch = new Stopwatch(); stopwatch.Start(); for (var i = 0; i < calls; i++) { resultsList.Add(Task.Factory.StartNew(DoStuff)); } // We lose a bit of accuracy, but if the loop is big enough, // it should not really matter while (resultsList.Any(task => !task.IsCompleted)) { Thread.Sleep(10); } stopwatch.Stop(); totalMs += stopwatch.ElapsedMilliseconds; } return totalMs; } } 
+9
source

Here, one of the filters in my project creates an audit in our database every time a user accesses a resource that needs to be checked.

Auditing, of course, is not what I would call "fire and forget." Remember, on ASP.NET, β€œfire and forget” means β€œI don't care if this code really runs or not . ” Thus, if your desired semantics is that audit can sometimes be absent, then (and only then) you can use fire and forget about your audits.

If you want your audits to be correct, either wait for the audit to complete before sending a response, or queue the audit information to a reliable store (for example, the Azure or MSMQ queue) and have an independent backend (for example, the Azure employee role or the Win32 service ) process the audits in this queue.

But if you want to live in danger (assuming audits can sometimes be skipped), you can mitigate the problems by registering your work with the ASP.NET runtime. Using the BackgroundTaskManager from my blog :

 public override void OnActionExecuting(ActionExecutingContext filterContext) { var request = filterContext.HttpContext.Request; var id = WebSecurity.CurrentUserId; BackgroundTaskManager.Run(() => { try { var audit = new Audit { Id = Guid.NewGuid(), IPAddress = request.UserHostAddress, UserId = id, Resource = request.RawUrl, Timestamp = DateTime.UtcNow }; var database = (new NinjectBinder()).Kernel.Get<IDatabaseWorker>(); database.Audits.InsertOrUpdate(audit); database.Save(); } catch (Exception e) { var username = WebSecurity.CurrentUserName; Debugging.DispatchExceptionEmail(e, username); } }); base.OnActionExecuting(filterContext); } 
+1
source

All Articles