在重定向时让任务完成?

时间:2021-02-19 10:43:04

标签: c# asp.net asp.net-core async-await task-parallel-library

我们正在开发一个整体式 Web 应用程序 - 非常有状态。它处理 HTTP 请求和长期存在的 SignalR 连接。 (在 ASP.NET Core 3.1 中 - 我们稍后将升级到 .NET 5。)

我们从登录页面重定向到我们的“主页”。主页需要一段时间来加载和初始化,之后它会与 SignalR 连接。我们在服务器端还有很多工作要做。在登录请求中执行服务器工作(在重定向到主页之前)会减慢登录速度。

“哦,那我们就用任务吧!”,我想。也就是说,将服务器工作放在一个任务中,将其保存在用户状态中,并让它与主页面的加载并行执行。像这样(简化):

public static async Task ServerSideInit()
{
    // do a lot of init work
}

// at the end of the controller handling the login page POST:
UserState.BackgroundTask = ServerSideInit();
Redirect(UrlToTheMainPage);

// when the main page connects via SignalR:
try {
    await UserState.BackgroundTask;
}
catch {
    // handle errors in the init work
}

这真的会加快速度。页面加载还是初始化工作先完成并不重要——我们等待任务。 ServerSideInit() 中的工作并不重要。如果发生某些事情并且主页永远不会连接,则 UserState(和 Task)将在超时后被销毁——这完全没问题。 (有一些注意事项。例如,我们必须使用 IServiceProvider 在 ServerSideInit() 中创建/处置作用域,因此我们在控制器之外获得了作用域 DbContext。但这没关系。)

但是我读到 ASP.NET Core 框架在结束 POST 请求时存在关闭任务的风险! (Do you have to await async methods?) 简单的 HostingEnvironment.QueueBackgroundWorkItem 不再可用。不过,有一个新的 BackgroundService 类。 (https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-3.1&tabs=visual-studio) 但是注册服务和排队作业似乎是一个非常麻烦的解决方案......我们只想触发一个需要几秒钟才能完成的任务,并在 ASP.NET Core 完成后让它继续运行处理完 POST 请求。

我对 ASP.NET Core 的经验不是很丰富……所以我非常感谢您提供一些意见!我的简单解决方案不起作用吗?任务会被框架终止吗?有没有更简单的方法来告诉框架“请不要碰这个任务”?或者 BackgroundService 是要走的路吗?

1 个答案:

答案 0 :(得分:3)

<块引用>

在登录请求中执行服务器工作(在重定向到主页之前)会减慢登录速度。 “哦,那我们就用任务吧!”,我想。也就是把服务端的工作放在一个Task中,保存在用户态,让它和主页面的加载并行执行。

因此,您需要进行请求外在工作。即,您的服务器所做的工作是在请求范围之外

您需要问自己的第一个问题是“这项工作需要完成吗?”换句话说,“我可以偶尔失去工作吗?”。如果出于正确性原因必须完成这项工作,那么只有一个真正的解决方案:asynchronous messaging。如果您对偶尔丢失工作感到满意(例如,如果主页会检测到 ServerSideInit 未完成并会在那时完成),那么您真正谈论的是缓存,并且有一个内存解决方案很好。

<块引用>

但后来我读到 ASP.NET Core 框架在结束 POST 请求时存在关闭任务的风险!

首先要认识到的是 shutdowns are normal。在常规部署、操作系统补丁等期间滚动更新......您的网络服务器迟早会自愿关闭,任何假设它会永远运行的代码本质上都是有缺陷的。

默认情况下,当所有请求都得到响应时,ASP.NET Core 会认为自己“可以安全关闭”。这是任何 HTTP 服务的合理行为,并且此逻辑扩展到每个 HTTP 框架,无论语言或运行时如何。但是,这显然是请求外部代码的问题。

因此,如果您的代码只是通过直接调用方法(或通过 Task.Run,另一个可悲的流行选项)来启动任务,那么它就很危险:ASP.NET 甚至不知道请求外部代码存在,并且会在请求时愉快地退出,突然终止该代码。

有一些权宜之计,例如 HostingEnvironment.QueueBackgroundWorkItem (pre-Core) 和 IHostedService / IHostApplicationLifetime (Core)。它们注册请求外部代码,以便 ASP.NET 知道它,并且在该代码完成之前不会关闭。然而,这些解决方案只能做到一半。因为它们在内存中,所以它们也很危险:ASP.NET 现在知道请求外部代码,但 HTTP 代理、负载平衡器和部署脚本不知道。

<块引用>

有没有更简单的方法告诉框架“请不要碰这个任务”?

回到这个回答开头的问题:“这个工作需要完成吗?”

如果这只是一个优化而不需要,那么只需使用 Task.Run(或 IHostedService)启动工作就足够了。不过,我不会将它保留在 UserState 中,因为 Task 不可序列化。

如果工作需要完成,那么构建一个asynchronous messaging解决方案。