在ASP.NET MVC中激发并忘记异步方法
本文关键字:忘记 异步方法 ASP NET MVC | 更新日期: 2023-09-27 18:29:37
对于即发即弃问题的一般答案(如here和here)不是使用async/await,而是使用传入同步方法的Task.Run
或TaskFactory.StartNew
然而,有时我想要即发即弃的方法是异步的,并且没有等效的同步方法。
更新说明/警告:正如Stephen Cleary在下面指出的那样,在发送响应后继续处理请求是危险的。原因是AppDomain可能会在工作仍在进行时关闭。有关更多信息,请参阅他回复中的链接。无论如何,我只是想提前指出这一点,这样我就不会让任何人走上错误的道路。
我认为我的情况是有效的,因为实际工作是由不同的系统(不同服务器上的不同计算机)完成的,所以我只需要知道消息已经留给了该系统。如果出现异常,服务器或用户对此无能为力,并且不会影响用户,我所需要做的就是参考异常日志并手动清理(或实现一些自动机制)。如果AppDomain被关闭,我将在远程系统中有一个剩余的文件,但我会在日常维护周期中提取它,因为我的网络服务器(数据库)不再知道它的存在,而且它的名称带有唯一的时间戳,所以它在逗留期间不会引起任何问题。
如果我能像Stephen Cleary指出的那样使用持久性机制,那将是一个理想的选择,但不幸的是,我现在没有。
我考虑过在保持请求打开的同时,假装DeleteFoo请求在客户端(javascript)上完成得很好,但我需要响应中的信息才能继续,所以它会阻碍事情的发展。
所以,最初的问题。。。
例如:
//External library
public async Task DeleteFooAsync();
在我的asp.net mvc代码中,我想以即发即弃的方式调用DeleteFooAsync——我不想让响应等待DeleteFooAsync.完成。如果DeleteFooAsync由于某种原因失败(或引发异常),则用户或程序对此无能为力,因此我只想记录一个错误。
现在,我知道任何异常都会导致未观察到的异常,所以我能想到的最简单的情况是:
//In my code
Task deleteTask = DeleteFooAsync()
//In my App_Start
TaskScheduler.UnobservedTaskException += ( sender, e ) =>
{
m_log.Debug( "Unobserved exception! This exception would have been unobserved: {0}", e.Exception );
e.SetObserved();
};
这样做有风险吗?
我能想到的另一个选择是制作我自己的包装,比如:
private void async DeleteFooWrapperAsync()
{
try
{
await DeleteFooAsync();
}
catch(Exception exception )
{
m_log.Error("DeleteFooAsync failed: " + exception.ToString());
}
}
然后用TaskFactory.StartNew调用它(可能是封装在异步操作中)。然而,每当我想以即发即弃方式调用异步方法时,这似乎都是大量的包装器代码。
我的问题是,以即发即弃方式调用异步方法的正确方式是什么?
更新:
好吧,我在我的控制器中发现了以下内容(并不是说控制器的操作需要异步,因为还有其他异步调用在等待):
[AcceptVerbs( HttpVerbs.Post )]
public async Task<JsonResult> DeleteItemAsync()
{
Task deleteTask = DeleteFooAsync();
...
}
导致表单异常:
未处理的异常:System.NullReferenceException:对象引用未设置为对象的实例。位于System.Web.ThreadContext.AssociateWithCurrentThread(BooleansetImpersonationContext)
这里讨论了这一点,似乎与SynchronizationContext和"在所有异步工作完成之前,返回的任务已转换到终端状态"有关。
因此,唯一有效的方法是:
Task foo = Task.Run( () => DeleteFooAsync() );
我之所以理解这一点,是因为StartNew为DeleteFooAsync获得了一个新的线程。
遗憾的是,Scott下面的建议在这种情况下不适用于处理异常,因为foo不再是DeleteFooAsync任务,而是task.Run中的任务,因此不处理DeleteFooAsync.中的异常。我的UnobservedTaskException最终会被调用,所以至少它仍然有效。
所以,我想问题仍然存在,你如何在asp.net mvc中实现即发即弃异步方法?
首先,让我指出,在ASP.NET应用程序中,"fire-and-forget"几乎总是一个错误。如果你不在乎DeleteFooAsync
是否真的完成了,那么"开火并忘记"只是一种可以接受的方法。
如果您愿意接受这个限制,我的博客上有一些代码可以在ASP.NET运行库中注册任务,并且它可以接受同步和异步工作。
您可以编写一个一次性包装器方法来记录异常,例如:
private async Task LogExceptionsAsync(Func<Task> code)
{
try
{
await code();
}
catch(Exception exception)
{
m_log.Error("Call failed: " + exception.ToString());
}
}
然后使用我博客中的BackgroundTaskManager
,如下所示:
BackgroundTaskManager.Run(() => LogExceptionsAsync(() => DeleteFooAsync()));
或者,你可以保留TaskScheduler.UnobservedTaskException
,并这样称呼它:
BackgroundTaskManager.Run(() => DeleteFooAsync());
从.NET 4.5.2开始,您可以执行以下
HostingEnvironment.QueueBackgroundWorkItem(async cancellationToken => await LongMethodAsync());
但它只能在ASP.NET域中工作
HostingEnvironment.QueueBackgroundWorkItem方法允许安排小的后台工作项目。ASP.NET跟踪这些项目防止IIS突然终止工作进程,直到所有后台工作项已完成。无法调用此方法在ASP.NET托管应用程序域之外。
更多信息:https://msdn.microsoft.com/en-us/library/ms171868(v=vs.110).aspx#v452
处理它的最佳方法是使用ContinueWith
方法并传入OnlyOnFaulted
选项。
private void button1_Click(object sender, EventArgs e)
{
var deleteFooTask = DeleteFooAsync();
deleteFooTask.ContinueWith(ErrorHandeler, TaskContinuationOptions.OnlyOnFaulted);
}
private void ErrorHandeler(Task obj)
{
MessageBox.Show(String.Format("Exception happened in the background of DeleteFooAsync.'n{0}", obj.Exception));
}
public async Task DeleteFooAsync()
{
await Task.Delay(5000);
throw new Exception("Oops");
}
我把我的信息框放在哪里,你就会把你的记录器放在哪里。