取消一个任务中的某个子任务

10

我正在尝试通过在任务内调用CancellationTokenSource.Cancel()方法来取消一个Task<>,但是我无法使它工作。

这是我使用的代码:

TaskScheduler ts = TaskScheduler.Current;

CancellationTokenSource cts = new CancellationTokenSource();

Task t = new Task( () =>
{
    Console.WriteLine( "In Task" );
    cts.Cancel();
}, cts.Token );

Task c1 = t.ContinueWith( antecedent =>
{
    Console.WriteLine( "In ContinueWith 1" );
}, CancellationToken.None, TaskContinuationOptions.OnlyOnRanToCompletion, ts );

Task c2 = c1.ContinueWith( antecedent =>
{
    Console.WriteLine( "In ContinueWith 2" );
}, TaskContinuationOptions.NotOnCanceled );

t.Start();

Console.ReadKey();

Environment.Exit( 1 );

这将打印输出:

In Task
In ContinueWith 1
In ContinueWith 2

我期望的是这样:

In Task

我有点不明白,任务只能在任务外部取消吗?这是什么情况?

我认为你不理解取消原则。请查看CancellationToken.ThrowIfCancellationRequested方法。 - Hamlet Hakobyan
那是因为我只是开始学习TPL。 - Intrepid
我认为这是一个非常好的问题,很惊讶自己能够复现。 - Johan Larsson
@JaroslawWaliszko 我无法使其非确定性,将其放入循环中并运行了10000次,结果相同。 - Johan Larsson
@Johan Larsson:另一方面,循环版本始终是可预测的,正如您所说。我是在谈论使用VS中的ctrl+f5运行几次,出现了各种输出的情况。也许这只是将结果刷新到控制台的情况。 - jwaliszko
我看到(并且预期)这种情况下的结果是确定性的。 - Matt Smith
2个回答

10

仅当以下条件之一满足时,任务才被视为“取消”:

  • 在任务开始执行之前,其取消令牌被取消。
  • 在任务执行期间,其取消令牌被取消,并且代码通过抛出OperationCancelledException(通常是通过调用cts.Token.ThrowIfCancellationRequested()的代码)协作地观察到了取消。

如果在cts.Cancel()之后添加了cts.Token.ThrowIfCancellationRequested()行,则事情将像您预期的那样运行。在您的示例中,取消操作发生在任务运行时,但任务没有观察到取消操作,因此任务的操作会一直运行直到完成。因此,该任务被标记为“已完成运行”。

您可以通过在继续执行代码中检查取消标记(cts.Token.IsCancellationRequested)来确定任务“已完成运行”但已取消(在任务完成期间或之后取消)的情况。另一个有时有帮助的选项是使用原始取消令牌作为继续执行的取消令牌(这样,如果前置任务未注意到取消,则继续执行至少会尊重取消),因为TPL会在连续执行之前将继续执行标记为已取消。


2
我想强调这里“已取消”的标准,因为我发现官方的MSDN文档有误导性。它暗示了一个任务可以通过抛出OperationCanceledException来取消自己。但是,只有在响应取消请求时才能使用此方法。即使您传递了取消令牌,除非您之前调用了CancellationTokenSource.Cancel(),否则这仍将被视为常规异常--任务将进入“IsFaulted”状态,而不是“IsCanceled”。 - nmclean
@nmclean,那不太对。使任务进入取消状态的要求是,任务的操作抛出 OperationCanceledException(OCE)(使用接受 CancellationToken(CT)的构造函数),并且 OCECTTaskCT 匹配。TaskCT 被认为是你传递给 Task.Factory.StartNew(...)Task.Run 的那个(即只是将其传递到 lambda 中不会让 TPL 知道它应该与任务关联)。注意:这与 async/await 不同,其中任何 OCE 都会导致任务被视为已取消。 - Matt Smith
2
@MattSmith 当我阅读文档时,这正是我所想的,但实际情况并非如此。你测试过吗?这是我的测试:http://pastebin.com/U7LcNvk0 我得到的输出是“Faulted”。如果我取消注释cts.Cancel,我会得到“Canceled”。 - nmclean
@nmclean,我还没有尝试过(我明天会试试你的代码),但是看了你的代码后我相信你(最初,我以为你没有将CT传递给StartNew(...)。所以,他们必须检查CancellationToken是否实际上已经取消了。有趣的是-谢谢你指出这一点。 - Matt Smith
@nmclean,我验证了我得到了相同的行为-所以你不能自我取消除非你给了任务访问CancellationTokenSource,这真的很有趣。我猜它给了调用者更多的控制权-他们确切地知道只有在他们取消任务时,任务才会进入取消状态。异步/等待不会给您这种控制权,因为任何OperationCanceledException都会导致任务进入已取消状态。请参见:https://dev59.com/XW_Xa4cB1Zd3GeqPzk95 - Matt Smith
显示剩余3条评论

0

这个代码片段按照您的预期工作。

var cts = new CancellationTokenSource();

            var t = new Task(() =>
            {
                Console.WriteLine("In Task");
                cts.Cancel();
            }, cts.Token);

            Task c1 = t.ContinueWith(antecedent =>
            {
                Console.WriteLine("In ContinueWith 1");
            }, CancellationToken.None, TaskContinuationOptions.OnlyOnCanceled/* by definition from MSDN here must be NotOnCanceled*/, TaskScheduler.Current);

            c1.ContinueWith(antecedent =>
            {
                Console.WriteLine("In ContinueWith 2");
            }, TaskContinuationOptions.NotOnCanceled);

            t.Start();
            Console.ReadKey();

            Environment.Exit(1);

但是似乎在 TaskContinuationOptions 枚举中存在错误。

NotOnCanceled 指定如果其前置任务已取消,继续任务不应该被调度。此选项对于多任务继续无效。

OnlyOnCanceled 指定仅当其前置任务已取消时才应安排继续任务。此选项对于多任务继续无效。


如果您注释掉 cts.Cancel(); 这一行,它将不会像我预期的那样执行第二个 ContinueWith()。我原本期望看到 In ContinueWith 2 的输出。 - Intrepid

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接