使用 CancellationToken 取消任务而不在任务中明确检查?

Posted

技术标签:

【中文标题】使用 CancellationToken 取消任务而不在任务中明确检查?【英文标题】:Using a CancellationToken to cancel a task without explicitly checking within the task? 【发布时间】:2019-12-01 23:04:40 【问题描述】:

背景:

我有一个启动长时间运行(和无状态)任务的 Web 应用程序:

var task = Task.Run(() => await DoWork(foo))

task.Wait();

因为它们运行时间很长,我需要能够从单独的网络请求中取消它们。

为此,我想使用 CancellationToken 并在取消令牌后立即抛出异常。但是,根据我的阅读,任务取消是cooperative,这意味着任务正在运行的代码必须明确检查令牌以查看是否已发出取消请求(例如CancellationToken.ThrowIfCancellation()

我想避免到处检查CancellationToken.ThrowIfCancellation(),因为这个任务很长并且要经过很多功能。我想我可以完成我想要创建一个明确的Thread,但我真的很想避免手动线程管理。那就是……

问题: 是否可以在任务被取消时自动抛出异常,如果没有,是否有任何好的替代方案(模式等)来减少CancellationToken.ThrowIfCancellation()对代码的污染?

我想避免这样的事情:

async Task<Bar> DoWork(Foo foo)

    CancellationToken.ThrowIfCancellation()

    await DoStuff1();

    CancellationToken.ThrowIfCancellation()

    await DoStuff2();

    CancellationToken.ThrowIfCancellation()

    await DoStuff3();
...


我觉得这个问题与this one 有很大的不同,因为我明确要求一种方法来最小化检查取消令牌的调用,接受的答案会响应“不时地,在函数内部,调用token.ThrowIfCancellationRequested()"

【问题讨论】:

"是否可以在任务被取消时自动抛出异常" 否。 CancellationToken 用于询问“请停止”,而不是用于强制终止任务。任务必须“听取”这些停止请求。 谢谢@Greg - 那么,有什么我可以使用(CancellationToken 或其他)来取消任务,还是我或多或少地坚持这种方法? 我不确定是否有任何替代方案,但我建议您遵循微软的模式。这将使与使用 Tasks 和 CancellationTokens 的其他库(如 ASP.NET 和 System.Data.SqlClient)的集成变得更加容易。 1) 通过子线程长时间运行的计算任务在 Web 应用程序中通常是个坏主意(AppPool 可以回收)2) 旋转将Task 提高到Wait() 有点违背了目的3) 是的,您“卡住” 可悲的是 @MickyD 1) 这是一个很好的观点 - 我实际上正在将一个 exe 转换为一个 Web 应用程序,所以我必须再分析一下。 2)没错,虽然我只是用它作为一个简单的例子。 3)¯\_(ツ)_/¯ 【参考方案1】:

是否可以在任务被取消时自动抛出异常,如果没有,是否有任何好的替代方案(模式等)来减少使用 CancellationToken.ThrowIfCancellation() 对代码的污染?

没有,也没有。所有取消都是合作的。取消代码的最佳方法是让代码响应取消请求。这是唯一好的模式。

我想我可以完成我想要创建一个明确的线程

不是真的。

此时,问题是“如何取消不可取消的代码?”答案取决于您希望系统有多稳定:

    不再需要时在单独的ThreadAbort 线程中运行代码。这是最容易实现的,但在应用程序不稳定方面最危险。坦率地说,如果您曾在应用中的任何位置调用 Abort,除了心跳/smoketest 检查等标准做法外,您还应该定期重启该应用。 在不再需要该 AppDomain 时,在单独的 AppDomainUnload 中运行代码。这更难实现(您必须使用远程处理),并且在核心世界中不是一个选项。事实证明,AppDomains 甚至没有像应有的那样保护包含的应用程序,因此任何使用这种技术的应用程序也需要定期重启。 当不再需要时,在单独的ProcessKill 进程中运行代码。这是最复杂的实现,因为您还需要实现某种形式的进程间通信。但取消不可取消的代码是唯一可靠的解决方案

如果你放弃不稳定的解决方案 (1) 和 (2),那么唯一剩下的解决方案 (3) 就是 的工作 - 方式,远远超过使代码可取消。

TL;DR:只需按照设计使用的方式使用取消 API。这是最简单、最有效的解决方案。

【讨论】:

我想知道你是否愿意插嘴,克利里先生。您提出的要点与我一直在研究的内容相匹配,但是对执行它们的困难/不稳定性的洞察力是我所需要的。阅读您在此处的其他帖子,我学到了很多东西,非常感谢您的意见。【参考方案2】:

如果您实际上只有一堆方法调用,您一个接一个地调用,您可以实现一个方法运行器,按顺序运行它们并在其间检查是否取消。

类似这样的:

public static void WorkUntilFinishedOrCancelled(CancellationToken token, params Action[] work)

    foreach (var workItem in work)
    
        token.ThrowIfCancellationRequested();
        workItem();
    

你可以这样使用它:

async Task<Bar> DoWork(Foo foo)

    WorkUntilFinishedOrCancelled([YourCancellationToken], DoStuff1, DoStuff2, DoStuff3, ...);        

这基本上可以满足您的需求。

【讨论】:

是的,我有点朝这个方向思考(装饰子任务)。它不像我的例子那么简单,但仍然很好的答案。 如果其中一种方法包含许多还需要定期任务取消检查语句的语句会怎样? 您可以在这个长时间运行的方法中使用 WorkUntilFinishedOrCancelled 并将这个方法拆分成更短的子任务。 WorkUntilFinishedOrCancelled 不处理任何与线程相关的东西(取消除外),因此您可以递归和重复调用它。【参考方案3】:

如果您对Thread.Abort 的implications 没问题(未处置一次性物品、未释放锁、应用程序状态已损坏),那么您可以通过中止任务的专用线程来实现非合作取消。

private static Task<TResult> RunAbortable<TResult>(Func<TResult> function,
    CancellationToken cancellationToken)

    var tcs = new TaskCompletionSource<TResult>();
    var thread = new Thread(() =>
    
        try
        
            TResult result;
            using (cancellationToken.Register(Thread.CurrentThread.Abort))
            
                result = function();
            
            tcs.SetResult(result);
        
        catch (ThreadAbortException)
        
            tcs.TrySetCanceled();
        
        catch (Exception ex)
        
            tcs.TrySetException(ex);
        
    );
    thread.IsBackground = true;
    thread.Start();
    return tcs.Task;

使用示例:

var cts = new CancellationTokenSource();
var task = RunAbortable(() => DoWork(foo), cts.Token);
task.Wait();

【讨论】:

我知道您可能只是在重复 OP 的代码,但将 Task 旋转到 Wait() 这有点违背了目的。此外,使用Task.Run 并使用现有的线程池可能比通过new Thread() 创建显式线程更好 @MickyD Aborting thread-pool threads 可能会更糟,因为在收到通知并中止线程时,原始工作项可能已经完成,因此池的线程已经转到另一个工作项。 1) .NET Matters 中的 Stephen Toub 引用写于 2006 年 3 月,predates@ 2008 年和 2009 年分别发布 987654331@ 公告和第一个 .NET 4.0 Public beta。 .NET 4.0 引入了TPL(其中引入了async/awaitTask)。本文引用了 .NET 2 中的显式类 ThreadPoolThreadPool.QueueUserWorkItem,而不是此处描述的 “线程池”。最重要的是,这篇文章提到了当线程被中止时,如果与QueueUserWorkItem 一起使用,ThreadPool 中可能发生的错误。 2) 您在生产代码中调用Thread.Abort() 并担心ThreadPool.QueueUserWorkItem 可能会开始出现异常行为?这样做会以与捕获和吞下未处理的异常完全相同的方式破坏损坏的进程。 @MickyD 我个人会使用合作取消,没有Thread.Abort。我认为我在回答中给出了足够的暗示,即中止线程是一种危险的做法!

以上是关于使用 CancellationToken 取消任务而不在任务中明确检查?的主要内容,如果未能解决你的问题,请参考以下文章

C#:使用 CancellationToken 取消 MySqlCommand,给出 NULLReferenceException

C#.net 5 使用取消令牌操作下载任务

取消 HttpClient 请求 - 为啥 TaskCanceledException.CancellationToken.IsCancellationRequested 为假?

CancellationToken 永远不会取消我长时间运行的加载数据功能

C#源码生成器的cancellationtoken啥时候取消?

.Net中异步任务的取消和监控