60

诸如此处此处之类的 即发即弃问题的一般答案不是使用异步/等待,而是使用Task.RunTaskFactory.StartNew传入同步方法。
但是,有时我想要一劳永逸的方法异步的,并且没有等效的同步方法。

更新说明/警告:正如 Stephen Cleary 在下面指出的,在您发送响应后继续处理请求是危险的。原因是 AppDomain 可能在该工作仍在进行时被关闭。有关更多信息,请参阅他的回复中的链接。无论如何,我只是想提前指出这一点,这样我就不会让任何人走错路。

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

如果我可以访问 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 中执行一劳永逸的异步方法?

4

3 回答 3

53

首先,让我指出“一劳永逸”在 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());
  }
}

然后使用BackgroundTaskManager我的博客中的:

BackgroundTaskManager.Run(() => LogExceptionsAsync(() => DeleteFooAsync()));

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

BackgroundTaskManager.Run(() => DeleteFooAsync());
于 2013-08-29T11:14:02.840 回答
15

从 .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

于 2016-04-20T16:11:16.677 回答
9

处理它的最好方法是使用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");
}

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

于 2013-08-29T05:29:46.887 回答