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



我有一个在队列中执行"长时间运行"任务的网络服务,有时会由于错误或验证不足(任务太大)而卡住。我需要及时取消这些任务,以便下一个客户端请求可以启动。

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

我已经读了很多关于使用BackgroundService, IHostedService的文章,以及大量文章,展示了取消异步不可取消任务的不同方法,但它们似乎只是从任务中"返回",让它运行。这对我不起作用,因为单个请求在我的小型服务器上最多可以占用 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();

尽管我尝试修复此类情况并在函数调用之前抛出异常,但我无法找到所有异常,我宁愿让我的客户收到错误,也不愿让服务器长时间卡住。我还分叉了一些第三方库以增强他们对CancellationToken的支持,但同样,有些总是会让我感到惊讶。我需要的是一个故障保护,确保Web服务不会卡住并变得无法使用。

我目前使用的系统看起来很简化,如下所示:

// 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时间太长。然后必须重新启动工作进程。

每当您有一个长时间运行的任务时,您首先应该将其从进程中移出。这意味着安排它通过另一个进程运行。例如,您可以创建一个工作线程服务,并通过某种事件通信模式远程排队工作,让它从数据库表中获取任务等。重要的是将其从您的 Web 进程中取出,因此它不会影响您的应用程序或其线程池。

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

您不想做的是在请求的上下文中运行任务,并且您绝对不想在没有等待的情况下这样做,我认为这可能是您的问题所在。换句话说,您正在执行以下操作:

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

这让请求继续并完成,但您已经衍生出一个不再直接控制的新线程。如果它最终完成,那没什么大不了的,但如果它挂起,那么你已经永久消耗了池中的一个线程,以及该线程保留的任何资源。此时您唯一能做的就是重新启动整个过程,因为没有办法再进入这个线程来杀死它。

取消令牌可以提供帮助,但它们不是魔法。它们表明已请求取消,但所有内容都必须支持取消。如果您调用的内容要么不支持将取消令牌传递到,要么不支持在某些子进程中取消,或者您甚至没有首先传递令牌,那么这一切都是徒劳的。这项工作将无限期地继续,直到它完成或出错。

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

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

最新更新