如果我有一个 api 控制器,并且我希望它尽快返回给用户,同时剥离一个线程来处理一些更长运行的进程,我想我不能像这样等待异步方法调用。
DoSomeNonAwaitedAsyncThing(data);
var result = await DoSomeNormalAsyncThing(data);
return result;
但是,当 api 控制器返回时,DoSomeNonAwaitedAsyncThing 代码将停止执行(没有异常或任何它停止运行的内容)。
如果我将 DoSomeNonAwaitedAsyncThing 的签名从异步任务更改为异步 void,那么线程将运行到完成,但它也会阻止 api 调用返回,直到非等待任务完成。
此外,如果我将调用方法的方式更改为 this(如下),那么该方法将运行完成,而不是在方法中途停止。
Task.Run(async () => { DoSomeNonAwaitedAsyncThing(data); });
我创建了一个带有项目的存储库来演示该问题。 https://github.com/JamesWebDev/AsyncThreadIssueExample
还值得注意的是,我将其添加到 Web 配置中,以便它应该允许在线程仍在运行时返回 api 调用。
<add key="aspnet:AllowAsyncDuringSyncStages" value="true"/>
DoSomeNonAwaitedAsyncThing代码将在api控制器返回时停止执行
问题很可能是因为DoSomeNonAwaitedAsyncThing
正在尝试重新输入 ASP.NET Classic请求上下文。默认情况下,await
将捕获当前上下文并恢复该上下文;在经典 ASP.NET 中,此"上下文"是 ASP.NET 经典请求上下文。但是,由于该请求上下文引用的请求已经完成,因此这可能会导致...并发症。
它与Task.Run
一起使用的原因是,DoSomeNonAwaitedAsyncThing
的当前上下文不再是 ASP.NET Classic 请求上下文;它现在是线程池上下文。当DoSomeNonAwaitedAsyncThing
内部的await
准备好继续时,线程池仍然存在。
作为最佳实践,应避免在 ASP.NET 上即发即弃(经典和核心都是如此)。正确的解决方案是让控制器写入可靠队列(例如 Azure 队列),并让独立的后台进程处理该操作(例如 Azure 函数)。独立后台进程有替代方案 - 经典进程为HostingEnvironment.QueueBackgroundWorkItem
和IRegisteredObject
,核心进程为IHostedService
和IHostApplicationLifetime
- 但最重要的是,控制器和后台进程之间至少需要一个可靠的队列。否则,您可能会在关机/回收期间丢失工作。