Use Task.Run instead of Delegate.BeginInvoke

2019-03-16 04:33发布

I have recently upgraded my projects to ASP.NET 4.5 and I have been waiting a long time to use 4.5's asynchronous capabilities. After reading the documentation I'm not sure whether I can improve my code at all.

I want to execute a task asynchronously and then forget about it. The way that I'm currently doing this is by creating delegates and then using BeginInvoke.

Here's one of the filters in my project with creates an audit in our database every time a user accesses a resource that must be audited:

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, which 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 rather not use the callback at all due to the fact that I do not need a result from the task that I am invoking asynchronously.

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

3条回答
太酷不给撩
2楼-- · 2019-03-16 04:34

Here's one of the filters in my project with creates an audit in our database every time a user accesses a resource that must be audited

Auditing is certainly not something I would call "fire and forget". Remember, on ASP.NET, "fire and forget" means "I don't care whether this code actually executes or not". So, if your desired semantics are that audits may occasionally be missing, then (and only then) you can use fire and forget for your audits.

If you want to ensure your audits are all correct, then either wait for the audit save to complete before sending the response, or queue the audit information to reliable storage (e.g., Azure queue or MSMQ) and have an independent backend (e.g., Azure worker role or Win32 service) process the audits in that queue.

But if you want to live dangerously (accepting that occasionally audits may be missing), you can mitigate the problems by registering the 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);
}
查看更多
小情绪 Triste *
3楼-- · 2019-03-16 04:38

If I understood your requirements correctly, you want to kick off a task and then forget about it. When the task completes, and if an exception occurred, you want to log it.

I'd use Task.Run to create a task, followed by ContinueWith to attach a continuation task. This continuation task will log any exception that was thrown from the parent task. Also, use TaskContinuationOptions.OnlyOnFaulted to make sure the continuation only runs if an exception occurred.

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-forget scenarios in ASP.NET are highly discouraged. See The Dangers of Implementing Recurring Background Tasks In ASP.NET

查看更多
一纸荒年 Trace。
4楼-- · 2019-03-16 05:00

It may sound a bit out of scope, but if you just want to forget after you launch it, why not using directly ThreadPool?

Something like:

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

I had to do some performance benchmarking for different async call methods and I found that (not surprisingly) ThreadPool works much better, but also that, actually, BeginInvoke is not that bad (I am on .NET 4.5). That's what I found out with the code at the end of the post. I did not find something like this online, so I took the time to check it myself. Each call is not exactly equal, but it is more or less functionally equivalent in terms of what it does:

  1. ThreadPool: 70.80ms
  2. Task: 90.88ms
  3. BeginInvoke: 121.88ms
  4. 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;
        }
    }
    
查看更多
登录 后发表回答