如何取消和释放拒绝取消任务的资源

How to cancel and release resources of task refusing to cancel

我有一个在队列中执行 "long-running" 任务的网络服务,有时会由于错误或验证不充分(任务太大)而卡住。我需要及时取消这些任务,以便下一个客户请求可以开始。

我目前使用 CancellationToken 超时 + 手动取消这些任务,我的代码中充斥着 ThrowIfCancellationRequested。有时代码会卡在某些收到不合理请求的第 3 方函数中,有时只是我代码中的一个错误导致取消不会发生。

我读了很多关于使用 BackgroundService, IHostedService 的文章,还有大量文章展示了取消异步不可取消任务的不同方法,但它们似乎只是 "return" 从任务中,离开它 运行。这对我不起作用,因为在我的小型服务器上,单个请求可能占用高达 90% 的 RAM 和 50% CPU,并且可能永远不会自行取消。所以这些解决方案会很快导致资源匮乏。

本文指出您无法取消不可取消的任务。 https://devblogs.microsoft.com/pfxteam/how-do-i-cancel-non-cancelable-async-operations/

编辑澄清:
我目前的解决方案是尊重 CancellationToken,它在 99% 的情况下都有效。失败的是这样的情况:

CT.ThrowIfCancellationRequested();
// The matrix Auu can become unreasonably large --> This 3rd party function takes minutes
var cholesky = SparseCholesky.Create(Auu, CSparse.ColumnOrdering.MinimumDegreeAtPlusA);

CT.ThrowIfCancellationRequested();

虽然我尝试修复这样的情况并在函数调用之前抛出异常,但我无法找到所有异常,我宁愿让我的客户收到错误也不愿让服务器长时间卡住 time.I 也分叉了一些第 3 方库以增强对 CancellationToken 的支持,但同样,有些库总是会让我感到惊讶。我需要的是确保网络服务不会卡住和变得不可用的故障保护。

我目前使用的系统看起来像这样简化:

// this code is in a singleton service in an ASP.NET core 3.0 web app
// this one is used to manually cancel from another method if requested
private CancellationTokenSource cancelSource;
public async Task Advance(...)
{
   //...
   cancelSource = new CancellationTokenSource())

   ComputeActive(); // This is not awaited, which lets the request finnish (what Chris Pratt mentioned in his answer)

}
private async Task ComputeActive()
{
    //...
    // this combined token handles automatic timeout ~90sec
    // but it will not help if the code is stuck in something that doesn't have CancellationTokens
    using (var timeoutSource = new CancellationTokenSource(Active.ComputeTimeLimit))
    using (var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(timeoutSource.Token, cancelSource.Token))
    {
        try
        {
            // this is the "long-running" task (0.1seconds to 40 seconds usually)
            var file = await Task.Run(() => product.Create(Active.Action, linkedSource.Token), linkedSource.Token);;
        }catch(...)
    }
}

那么我的解决方案是什么? Thread.Abort()?还是重新启动整个应用程序更好?

解决方案:我按照答案中给出的建议将任务移动到另一个进程解决了这个问题,然后我可以 Environment.Exit(0) 当用 CancellationToken 取消花费的时间太长时。然后必须重新启动工作进程。

任何时候你有一个很长的 运行ning 任务,你首先应该把它从进程中取出来。这意味着通过另一个进程将其调度到 运行。例如,您可以创建一个辅助服务并通过某种事件通信模式远程排队工作,让它从数据库 table 中获取任务,等等。重要的是将它从您的 Web 进程中取出,所以它不会影响您的应用程序或其线程池。

一个更简单但不太可靠的解决方案是在应用程序本身中使用托管服务 运行ning。这至少提供了一定程度的隔离并且不占用请求,但它仍然在同一个进程中,所以它使用相同的线程池、内存等。

不想做的是运行请求上下文中的任务,你肯定不想在没有等待的情况下这样做它,我认为这可能是您的问题所在。换句话说,你正在做类似的事情:

Task.Run(x => MyLongRunningMethod());

让请求继续并完成,但是您已经分出了一个您不再有任何直接控制权的新线程。如果它 最终 完成,这没什么大不了的,但如果它挂起,那么你已经永久地消耗了池中的一个线程,以及该线程持有的任何资源.此时您可以做的唯一一件事就是重新启动整个进程,因为无法再进入该线程以终止它。

取消令牌可以提供帮助,但它们并不神奇。它们表示请求取消,但所有的一切都必须支持取消。如果您调用的对象不支持传入取消令牌,不支持某些子流程中的取消,或者您甚至没有首先传递令牌,那么这一切都是为了没有。这项工作将无限期地继续,直到它完成或出错。

总而言之,不要使用Task.Run除非你有办法取消任务,它总是会完成,或者你实际上正在等待它。即便如此,您也不应该 ever 在 Web 应用程序中使用它,因为在最好的情况下,您只是将一个线程换成另一个线程,而在最坏的情况下,您长时间消耗池中的线程,降低了您的 Web 应用程序的潜在吞吐量。

将工作移出请求管道,理想情况下将其完全移出流程。