[英]Use Task.Run instead of Delegate.BeginInvoke
我最近将我的项目升级到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
)改进此代码?
它可能听起来有点超出范围,但如果您只是想在启动后忘记,为什么不直接使用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.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; } }
如果我正确地理解了您的要求,您想要启动任务然后忘记它。 任务完成后,如果发生异常,您需要记录它。
我将使用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.NET中实现重复后台任务的危险
这是我项目中的一个过滤器,每次用户访问必须审计的资源时,都会在我们的数据库中创建一个审计
审计肯定不是我称之为“火与忘记”的东西。 请记住,在ASP.NET上, “fire and forget”意味着“我不关心这段代码是否实际执行” 。 因此,如果您希望的语义可能偶尔会丢失审核,那么(并且只有这样)您可以使用fire并忘记审核。
如果要确保审核完全正确,则在发送响应之前等待审核保存完成,或者将审核信息排队到可靠存储(例如,Azure队列或MSMQ)并具有独立后端(例如,Azure worker角色或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);
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.