使用Task.Run而不是Delegate.BeginInvoke

8
我最近将我的项目升级到了 ASP.NET 4.5,并且我已经等待了很长时间来使用 4.5 的异步能力。阅读文档后,我不确定是否能够改进我的代码。
我想异步执行一个任务,然后忘记它。我目前使用的方法是创建委托,然后使用BeginInvoke。
这是我项目中的一个过滤器,在每次用户访问必须进行审计的资源时,在我们的数据库中创建一个审计记录:
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);
}

但是为了完成这个异步任务,我需要总是定义一个回调函数,它看起来像这样:

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);
    }
}

我不需要异步调用的结果,因此我宁愿不使用回调。如何使用Task.Run()(或asyncawait)改进此代码?
3个回答

13

如果我正确理解您的要求,您想启动一个任务并忘记它。当任务完成且发生异常时,您想要记录它。

我会使用Task.Run创建一个任务,然后使用ContinueWith附加一个继续任务。这个继续任务将记录从父任务抛出的任何异常。此外,使用TaskContinuationOptions.OnlyOnFaulted确保继续仅在发生异常时运行。

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);

作为旁注,ASP.NET中的后台任务和fire-and-forget场景极其不推荐。请参阅在ASP.NET中实现重复后台任务的危险性


谢谢您的快速回复,这正是我所需要的。我一定会检查所有异步代码,并确定是否有必要将其作为后台任务执行。 - dimiguel

11

虽然可能有点超出范围,但如果你只是想在启动后忘记它,为什么不直接使用 ThreadPool

例如:

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

我需要对不同的异步调用方法进行性能基准测试,并发现(并不意外)ThreadPool的效果更好,但实际上,BeginInvoke也不错(我的.NET版本是4.5)。这就是我在文章末尾附带的代码中所发现的。我没有在网上找到类似的东西,所以我花时间自己检查了它。每个调用不完全相等,但从其功能上来看,它们更或多或少是等效的:

  1. ThreadPool:70.80毫秒
  2. Task:90.88毫秒
  3. BeginInvoke: 121.88毫秒
  4. Thread:4657.52毫秒

    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;
        }
    }
    

2
以下是我项目中的一个过滤器,每当用户访问必须进行审计的资源时,在我们的数据库中创建一条审计记录。
审计绝对不是我所说的“点火并忘记”的东西。请记住,在ASP.NET上,“点火并忘记”意味着“我不关心这段代码是否实际执行”。因此,如果您希望审计偶尔可能丢失,则只能使用“点火并忘记”进行审计。
如果您想确保所有审计都是正确的,请在发送响应之前等待审计保存完成,或将审计信息排队到可靠的存储(例如Azure队列或MSMQ),并有一个独立的后端(例如Azure工作角色或Win32服务)处理该队列中的审计。
但是,如果你想要冒险(接受偶尔可能会缺少审计的事实),你可以通过在ASP.NET运行时中注册该工作来减轻问题。使用我博客中的BackgroundTaskManager
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);
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接