使用任务.运行而不是Delegate.BeginInvoke

本文关键字:Delegate BeginInvoke 任务 运行 | 更新日期: 2023-09-27 17:54:12

我最近将我的项目升级为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)改进这个代码?

使用任务.运行而不是Delegate.BeginInvoke

如果我正确理解了您的需求,您想要开始一项任务,然后忘记它。当任务完成时,如果发生了异常,则需要对其进行记录。

我将使用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 非常不鼓励使用。参见在ASP中实现重复出现的后台任务的危险。净

这可能听起来有点超出范围,但如果您只是想在启动后忘记它,为什么不直接使用ThreadPool呢?

类似:

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

我不得不对不同的异步调用方法做一些性能基准测试,我发现(毫不奇怪)ThreadPool工作得更好,但实际上,BeginInvoke也没有那么糟糕(我使用的是。net 4.5)。这就是我在文章末尾的代码中发现的。我在网上找不到这样的东西,所以我花时间自己去查了一下。每个调用并不完全相等,但就其功能而言,它们在功能上或多或少是相等的:

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

这是我项目中的一个过滤器,每当用户访问必须审计的资源时,都会在数据库中创建一个审计

审计当然是不是我称之为"火了就忘了"的东西。记住,在ASP上。NET中,"即发即弃"的意思是"我不在乎这段代码是否实际执行"。因此,如果您想要的语义是审计可能偶尔会丢失,那么(只有在这种情况下)您可以对审计使用fire and forget。

如果你想确保你的审计都是正确的,那么在发送响应之前等待审计保存完成,或者将审计信息排队到可靠的存储(例如,Azure队列或MSMQ),并让一个独立的后端(例如,Azure worker角色或Win32服务)在该队列中处理审计。

但是,如果您想要危险地生活(接受偶尔可能会丢失审计),您可以通过向ASP注册工作来减轻问题。网运行时。使用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);
}