在asp.net mvc中点火并忘记异步方法

aca*_*lon 52 .net c# asp.net asynchronous async-await

一般的答案,如这里这里发射后不管的问题是不使用异步/ AWAIT,但使用Task.RunTaskFactory.StartNew在同步方式传递来代替.
但是,有时我想要发射并忘记的方法是异步的,并且没有等效的同步方法.

更新注释/警告:正如Stephen Cleary在下面指出的那样,在您发送响应后继续处理请求是危险的.原因是AppDomain可能会在该工作仍在进行时关闭.有关详细信息,请参阅其响应中的链接.无论如何,我只是想提前指出,所以我不会让任何人走错路.

我认为我的情况是有效的,因为实际工作是由不同的系统(不同服务器上的不同计算机)完成的,所以我只需要知道该消息已经留给该系统.如果存在异常,则服务器或用户无法对其进行任何操作并且不会影响用户,我需要做的就是参考异常日志并手动清理(或实现一些自动机制).如果关闭AppDomain,我将在远程系统中有一个残留文件,但是我将在常规维护周期中选择它,因为我的Web服务器(数据库)不再知道它的存在,并且它的名称是唯一的时间戳,它仍然会徘徊不会导致任何问题.

如果我能像Stephen Cleary所指出的那样访问持久性机制,那将是理想的,但不幸的是我现在还没有.

我认为只是假装DeleteFoo请求在客户端(javascript)已经完成,同时保持请求打开,但我需要响应中的信息继续,所以它会保持原状.

那么,原来的问题......

例如:

//External library
public async Task DeleteFooAsync();
Run Code Online (Sandbox Code Playgroud)

在我的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();
};
Run Code Online (Sandbox Code Playgroud)

这样做有风险吗?

我能想到的另一个选择是制作我自己的包装器,例如:

private void async DeleteFooWrapperAsync()
{
    try
    {
        await DeleteFooAsync();
    }
    catch(Exception exception )
    {
        m_log.Error("DeleteFooAsync failed: " + exception.ToString());
    }
}
Run Code Online (Sandbox Code Playgroud)

然后使用TaskFactory.StartNew调用它(可能包含在异步操作中).然而,每次我想以一种即发即忘的方式调用异步方法时,这看起来像很多包装器代码.

我的问题是,以一种"一劳永逸"的方式调用异步方法的正确方法是什么?

更新:

好吧,我在控制器中发现了以下内容(不是控制器操作需要异步,因为还有其他等待的异步调用):

[AcceptVerbs( HttpVerbs.Post )]
public async Task<JsonResult> DeleteItemAsync()
{
    Task deleteTask = DeleteFooAsync();
    ...
}
Run Code Online (Sandbox Code Playgroud)

导致形式的例外:

未处理的异常:System.NullReferenceException:未将对象引用设置为对象的实例.在System.Web.ThreadContext.AssociateWithCurrentThread(BooleansetImpersonationContext)

这在这里讨论,似乎与SynchronizationContext和'返回的任务在所有异步工作完成之前转换到终端状态'有关.

所以,唯一有效的方法是:

Task foo = Task.Run( () => DeleteFooAsync() );
Run Code Online (Sandbox Code Playgroud)

我对其工作原理的理解是因为StartNew为DeleteFooAsync获取了一个新线程.

遗憾的是,Scott的建议在这种情况下不能用于处理异常,因为foo不再是DeleteFooAsync任务,而是来自Task.Run的任务,所以不处理DeleteFooAsync的异常.我的UnobservedTaskException最终会被调用,所以至少它仍然有效.

所以,我想这个问题仍然存在,你如何在asp.net mvc中点击并忘记异步方法?

Ste*_*ary 50

首先,让我指出"火与遗忘"在ASP.NET应用程序中几乎总是一个错误.如果您不关心DeleteFooAsync实际是否完成,"火与忘记"只是一种可接受的方法.

如果你愿意接受这个限制,我的博客上会有一些代码可以在ASP.NET运行时注册任务,它同时接受同步和异步工作.

您可以编写一次性包装器方法来记录异常,如下所示:

private async Task LogExceptionsAsync(Func<Task> code)
{
  try
  {
    await code();
  }
  catch(Exception exception)
  {
    m_log.Error("Call failed: " + exception.ToString());
  }
}
Run Code Online (Sandbox Code Playgroud)

然后使用BackgroundTaskManager我的博客:

BackgroundTaskManager.Run(() => LogExceptionsAsync(() => DeleteFooAsync()));
Run Code Online (Sandbox Code Playgroud)

或者,你可以保持TaskScheduler.UnobservedTaskException并像这样调用它:

BackgroundTaskManager.Run(() => DeleteFooAsync());
Run Code Online (Sandbox Code Playgroud)

  • @Andyz Smith:一个简单的人为例子 - 每个第n个用户图像检索请求触发的维护周期,用于索引/合理化主文件存储库是远程存储的Web服务器上频繁使用的文件的本地缓存,例如Azure文件存储.用户不关心它.如果用户失败,它不会真正影响用户.如果AppDomain关闭并重新启动,则无需担心,维护将在下一个周期进行. (4认同)
  • 谢谢,这很棒.我从博客中看到,这是非常糟糕的原因是因为AppDomain可以在即发即弃操作中被拆除.在我的情况下,DeleteFooAsync由另一个系统处理,因此知道该消息已离开我的进程.之所以发生火灾,是因为它是一个相当长时间运行的操作,我不想让用户等待响应,用户和服务器都无法做任何关于异常的事情.对用户没有不良影响.再次感谢,我会考虑一下. (3认同)

Kor*_*yem 14

从.NET 4.5.2开始,您可以执行以下操作

HostingEnvironment.QueueBackgroundWorkItem(async cancellationToken => await LongMethodAsync());
Run Code Online (Sandbox Code Playgroud)

但它只适用于ASP.NET域

HostingEnvironment.QueueBackgroundWorkItem方法允许您安排小型后台工作项.ASP.NET跟踪这些项目并阻止IIS突然终止工作进程,直到所有后台工作项都完成.无法在ASP.NET托管应用程序域之外调用此方法.

更多信息:https://msdn.microsoft.com/en-us/library/ms171868(v = vs.110).aspx#v452


Sco*_*ain 8

处理它的最佳方法是使用该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");
}
Run Code Online (Sandbox Code Playgroud)

在我放置信息框的地方,你会把你的记录器.

  • 听起来不错.是否有规范参考最佳实践,有人可以提供这种技术? (4认同)