诸如here和here到即发即弃问题的一般答案不是使用async / await,而是使用Task.Run
或TaskFactory.StartNew
代之以传递同步方法。
但是,有时我想要 fire-and-forget 的方法是异步的,并且没有等效的同步方法。
更新注释/警告:正如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)
这是here讨论的,似乎与SynchronizationContext有关,并且&#39;返回的Task在所有异步工作完成之前已转换为终端状态&#39;。
所以,唯一有效的方法是:
Task foo = Task.Run( () => DeleteFooAsync() );
我对其工作原理的理解是因为StartNew为DeleteFooAsync获取了一个新线程。
遗憾的是,Scott的建议在这种情况下不能用于处理异常,因为foo不再是DeleteFooAsync任务,而是来自Task.Run的任务,所以不处理来自DeleteFooAsync的异常。我的UnobservedTaskException最终会被调用,所以至少它仍然有效。所以,我想这个问题仍然存在,你如何在asp.net mvc中点击并忘记异步方法?
答案 0 :(得分:49)
首先,让我指出“火与忘记”几乎总是在ASP.NET应用程序中出错。如果您不关心DeleteFooAsync
是否实际完成,“火与忘记”只是一种可接受的方法。
如果您愿意接受这个限制,我会some code on my blog向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());
答案 1 :(得分:14)
从.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
答案 2 :(得分: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");
}
在我放置信息框的地方,你会把你的记录器。