在ASP.NET MVC中激发并忘记异步方法

本文关键字:忘记 异步方法 ASP NET MVC | 更新日期: 2023-09-27 18:29:37

对于即发即弃问题的一般答案(如here和here)不是使用async/await,而是使用传入同步方法的Task.RunTaskFactory.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 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");
}

我把我的信息框放在哪里,你就会把你的记录器放在哪里。