当我在异步方法中调用CancellationTokenSource的Cancel方法时,为什么任务没有被取消?

11

我创建了一个围绕 CancellationTokenCancellationTokenSource 的小封装。我遇到的问题是,CancellationHelperCancelAsync 方法并不能按照预期工作。

我在 ItShouldThrowAExceptionButStallsInstead 方法中遇到了这个问题。为了取消运行中的任务,它调用了 await coordinator.CancelAsync();,但实际上任务并没有被取消,并且在 task.Wait 上也没有抛出异常。

ItWorksWellAndThrowsException 看起来工作得很好,它使用的是 coordinator.Cancel,这根本不是异步方法。

问题在于,为什么当我在异步方法中调用 CancellationTokenSource 的 Cancel 方法时,任务没有被取消呢?

不要让 waitHandle 混淆你的视听,它只是为了防止任务过早结束。

代码自证。

using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

namespace TestCancellation
{
    class Program
    {
        static void Main(string[] args)
        {
            ItWorksWellAndThrowsException();
            //ItShouldThrowAExceptionButStallsInstead();
        }

        private static void ItShouldThrowAExceptionButStallsInstead()
        {
            Task.Run(async () =>
            {
                var coordinator = new CancellationHelper();
                var waitHandle = new ManualResetEvent(false);

                var task = Task.Run(() =>
                {
                    waitHandle.WaitOne();

                    //this works well though - it throws
                    //coordinator.ThrowIfCancellationRequested();

                }, coordinator.Token);

                await coordinator.CancelAsync();
                //waitHandle.Set(); -- with or without this it will throw
                task.Wait();
            }).Wait();
        }

        private static void ItWorksWellAndThrowsException()
        {
            Task.Run(() =>
            {
                var coordinator = new CancellationHelper();
                var waitHandle = new ManualResetEvent(false);

                var task = Task.Run(() => { waitHandle.WaitOne(); }, coordinator.Token);

                coordinator.Cancel();
                task.Wait();
            }).Wait();
        }
    }

    public class CancellationHelper
    {
        private CancellationTokenSource cancellationTokenSource;
        private readonly List<Task> tasksToAwait;

        public CancellationHelper()
        {
            cancellationTokenSource = new CancellationTokenSource();
            tasksToAwait = new List<Task>();
        }

        public CancellationToken Token
        {
            get { return cancellationTokenSource.Token; }
        }

        public void AwaitOnCancellation(Task task)
        {
            if (task == null) return;

            tasksToAwait.Add(task);
        }

        public void Reset()
        {
            tasksToAwait.Clear();
            cancellationTokenSource = new CancellationTokenSource();
        }

        public void ThrowIfCancellationRequested()
        {
            cancellationTokenSource.Token.ThrowIfCancellationRequested();
        }

        public void Cancel()
        {
            cancellationTokenSource.Cancel();

            Task.WaitAll(tasksToAwait.ToArray());
        }

        public async Task CancelAsync()
        {
            cancellationTokenSource.Cancel();

            try
            {
                await Task.WhenAll(tasksToAwait.ToArray());
            }
            catch (AggregateException ex)
            {
                ex.Handle(p => p is OperationCanceledException);
            }
        }
    }
}

1
你在哪里调用了 CancellationHelper.ThrowIfCancellationRequested() - Matthew Watson
我认为对于取消令牌的工作原理存在误解...即对于Task.Run,令牌仅在任务构建期间相关,在运行时,您必须自己检查令牌... - user57508
@AndreasNiedermair 我明白你的意思,但是如果任务完成并且在执行过程中调用了 CancellationTokenSource.Cancel,那么它的 task.Result 应该是 TaskStatus.Canceled,不是吗? - Balázs Szántó
如果我理解正确,一旦任务开始执行,task.Status就不会是TaskStatus.Canceled,即使调用了Cancel,我仍然需要检查CancellationTokenSource.IsCancellationRequested - Balázs Szántó
@boli 如果你加入 token.ThrowIfCancellationRequested(),就有可能出现 TaskStatus.Faulted 的情况。 - user57508
1
可能是如何中止/取消TPL任务?的重复问题。 - Liam
1个回答

13

在.NET中,取消操作是协作的。

这意味着持有CancellationTokenSource的人会发出取消信号,而持有CancellationToken的人需要检查是否已经发出了取消信号(可以通过轮询CancellationToken或注册一个委托来在发出取消信号时运行它)。

在您的Task.Run中,您使用CancellationToken作为参数,但是您没有在任务本身内部检查它,因此如果在任务开始之前就发出了令牌信号,则该任务将被取消。

要在任务正在运行时取消它,您需要检查CancellationToken

var task = Task.Run(() =>
{
    token.ThrowIfCancellationRequested();
}, token);

在您的情况下,您会阻止在一个ManualResetEvent上,因此您将无法检查CancellationToken。您可以注册一个委托到CancellationToken,以释放复位事件:

token.Register(() => waitHandle.Set())

var task = Task.Run(() => { token.ThrowIfCancellationRequested(); }, token); 看起来不错。但是在哪里捕获异常呢? - swifty

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