使用Task.Run而不是Delegate.BeginInvoke

时间:2014-03-27 11:37:58

标签: c# asynchronous

我最近将项目升级到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 个答案:

答案 0 :(得分:11)

这可能听起来有点超出范围,但如果您只是想在启动后忘记,为什么不直接使用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;
        }
    }
    

答案 1 :(得分:10)

如果我正确理解了您的要求,您想要启动任务然后忘记它。任务完成后,如果发生异常,您需要记录它。

我使用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中的后台任务和“即发即弃”场景高度不鼓励。见The Dangers of Implementing Recurring Background Tasks In ASP.NET

答案 2 :(得分:1)

  

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

审核肯定是而不是我会称之为&#34;火与忘记&#34;。请记住,在ASP.NET上,"fire and forget" means "I don't care whether this code actually executes or not"。因此,如果您希望的语义可能偶尔会丢失审核,那么(并且只有这样)您可以使用fire并忘记进行审核。

如果要确保审核完全正确,则在发送响应之前等待审核保存完成,或者将审核信息排队到可靠的存储(例如,Azure队列或MSMQ)并具有独立的后端(例如,Azure辅助角色或Win32服务)处理该队列中的审核。

但是如果你想要危险地生活(接受偶尔可能缺少审计),你可以通过向ASP.NET运行时注册工作来缓解这些问题。使用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);
}