Use Task.Run instead of Delegate.BeginInvoke

后端 未结 3 436
被撕碎了的回忆
被撕碎了的回忆 2021-02-06 08:11

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 wh

相关标签:
3条回答
  • 2021-02-06 08:17

    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);
    }
    
    0 讨论(0)
  • 2021-02-06 08:34

    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;
          }
      }
      
    0 讨论(0)
  • 2021-02-06 08:39

    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

    0 讨论(0)
提交回复
热议问题