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

时间:2019-10-22 13:12:56

标签: c# asp.net-core .net-core-3.0

我有一个Web服务,该服务在队列中执行“长时间运行”的任务,有时由于错误或验证不足(任务太大)而陷入困境。我需要及时取消这些任务,以便下一个客户请求可以开始。

我目前超时,并使用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进行任务。然后必须重新启动工作进程。

1 个答案:

答案 0 :(得分:3)

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

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

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

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

让请求继续执行并结束,但是您已经创建了一个新线程,您不再可以对其进行直接控制。如果最终完成,则没什么大不了的,但是如果它挂起,则说明您已永久消耗了池中的线程以及该线程所持有的所有资源。此时,您 唯一能做的就是重新启动整个过程,因为再也找不到办法杀死该线程了。

取消令牌可以提供帮助,但并不是魔术。它们表明已请求取消,但一直以来的所有操作都必须支持取消。如果您要调用的内容要么不支持传递取消标记,要么不支持某些子过程中的取消,或者甚至根本没有传递该标记,那么这一切都是为了一无所有。该工作将无限期继续,直到完成或出现错误。

简短的说,除非您有取消任务的方法,否则不要使用Task.Run,它会一直完成,或者您实际上正在等待它。即使那样,您也不应在Web应用程序中永远使用它,因为在最佳情况下,您只是将一个线程换为另一线程,而在最坏的情况下,您正在消耗长时间从池中线程化线程,从而降低了Web应用程序的潜在吞吐量。

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