使用任务.运行而不是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()
(或async
和await
)改进这个代码?
如果我正确理解了您的需求,您想要开始一项任务,然后忘记它。当任务完成时,如果发生了异常,则需要对其进行记录。
我将使用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)。这就是我在文章末尾的代码中发现的。我在网上找不到这样的东西,所以我花时间自己去查了一下。每个调用并不完全相等,但就其功能而言,它们在功能上或多或少是相等的:
-
ThreadPool
: 70.80ms -
Task
: 90.88ms -
BeginInvoke
: 121.88ms -
Thread
: 4657.52mspublic 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);
}