Task构造函数中的取消令牌:为什么需要?

230
某些 System.Threading.Tasks.Task 的构造函数会将 CancellationToken 作为参数传入:
CancellationTokenSource source = new CancellationTokenSource();
Task t = new Task (/* method */, source.Token);

这让我感到困惑的是,在方法内部没有办法真正获取传入的令牌(例如,没有像Task.CurrentTask.CancellationToken这样的东西)。该令牌必须通过其他机制(例如状态对象或在lambda中捕获)提供。 那么,在构造函数中提供取消标记有什么作用呢?
4个回答

263
将一个CancellationToken传递到Task构造函数中会将其与该任务关联起来。
引用自MSDN的Stephen Toub的回答
这有两个主要好处: 1. 如果在Task开始执行之前请求取消令牌,那么Task将不会执行。它不会转换为Running,而是立即转换为Canceled状态。这避免了运行任务的成本,如果任务在运行时只是被取消。 2. 如果任务的主体也在监视取消令牌并抛出包含该令牌的OperationCanceledException(这就是ThrowIfCancellationRequested的作用),那么当任务看到OperationCanceledException时,它会检查OperationCanceledException的令牌是否与Task的令牌匹配。如果匹配,那么该异常被视为协作取消的确认,并且Task转换为Canceled状态(而不是Faulted状态)。

29
构造函数在内部使用令牌进行取消处理。如果您的代码需要访问该令牌,则需要负责将其传递给自己。我强烈建议阅读Parallel Programming with Microsoft .NET book at CodePlex,以了解更多信息。
书中CTS的示例用法:
CancellationTokenSource cts = new CancellationTokenSource();
CancellationToken token = cts.Token;

Task myTask = Task.Factory.StartNew(() =>
{
    for (...)
    {
        token.ThrowIfCancellationRequested();

        // Body of for loop.
    }
}, token);

// ... elsewhere ...
cts.Cancel();

3
如果您不将令牌作为参数传递,会发生什么?看起来行为将是相同的,没有任何目的。 - sergtk
2
@sergdev:你需要传递令牌以在任务和调度程序中进行注册。不传递它并使用它将导致未定义的行为。 - user7116
3
经过测试发现:如果没有将token作为参数传递,myTask.IsCanceled和myTask.Status的值不同。此时,状态将会是“失败”(failed),而不是“取消”(canceled)。尽管如此,两种情况下抛出的异常相同,都是OperationCanceledException。 - Olivier de Rivoyre
2
如果我不调用 token.ThrowIfCancellationRequested(); 会怎么样?在我的测试中,行为是相同的。有什么想法吗? - machinarium
@machinarium @sergtk:如果您将CancellationTokenSource中的令牌传递到Task构造函数中,当调用cts.Cancel()时,无论您在方法体中做什么(或不做任何事情),该任务都将被取消并结束。默认情况下,取消的任务状态设置为_Faulted_,以表示它已经未经您同意结束。如果您通过token.ThrowIfCancellationRequested()监视有意的取消,则当调用tcs.Cancel()时,任务的状态将设置为_Canceled_,因为您的代码已确认其知道取消请求并采取了适当的操作。 - CobaltBlue
3
当调用cts.Cancel()时,无论你做什么,该任务都将被取消并结束。不对。如果在任务开始之前已经取消了该任务,则该任务会被标记为_Canceled_。如果任务体根本没有检查任何令牌,它将运行到完成,结果为_RanToCompletion_状态。如果任务体抛出OperationCancelledException(例如通过ThrowIfCancellationRequested),那么Task将检查该异常的CancellationToken是否与与该任务关联的CancellationToken相同。如果是,则该任务被标记为_Canceled_。否则,它将被标记为_Faulted_。 - Wolfzoon

7
取消操作并不像许多人想象的那样简单。在 msdn 的这篇博客文章中解释了其中的一些细节:
例如:
在 Parallel Extensions 和其他系统中,某些情况下需要唤醒一个被阻塞的方法,而这些原因并不是由用户的显式取消引起的。例如,如果一个线程因集合为空而被阻塞在 blockingCollection.Take() 上,而另一个线程随后调用 blockingCollection.CompleteAdding(),则第一个调用应该被唤醒并抛出一个 InvalidOperationException,表示使用不正确。
参考链接:Parallel Extensions 中的取消操作

3
以下是一个代码示例,演示了Max Galkin接受答案中提到的两个要点:
class Program
{
    static void Main(string[] args)
    {
        Console.WriteLine("*********************************************************************");
        Console.WriteLine("* Start canceled task, don't pass token to constructor");
        Console.WriteLine("*********************************************************************");
        StartCanceledTaskTest(false);
        Console.WriteLine();

        Console.WriteLine("*********************************************************************");
        Console.WriteLine("* Start canceled task, pass token to constructor");
        Console.WriteLine("*********************************************************************");
        StartCanceledTaskTest(true);
        Console.WriteLine();

        Console.WriteLine("*********************************************************************");
        Console.WriteLine("* Throw if cancellation requested, don't pass token to constructor");
        Console.WriteLine("*********************************************************************");
        ThrowIfCancellationRequestedTest(false);
        Console.WriteLine();

        Console.WriteLine("*********************************************************************");
        Console.WriteLine("* Throw if cancellation requested, pass token to constructor");
        Console.WriteLine("*********************************************************************");
        ThrowIfCancellationRequestedTest(true);
        Console.WriteLine();

        Console.WriteLine();
        Console.WriteLine("Test Completed!!!");
        Console.ReadKey();
    }

    static void StartCanceledTaskTest(bool passTokenToConstructor)
    {
        Console.WriteLine("Creating task");
        CancellationTokenSource tokenSource = new CancellationTokenSource();
        Task task = null;
        if (passTokenToConstructor)
        {
            task = new Task(() => TaskWork(tokenSource.Token, false), tokenSource.Token);

        }
        else
        {
            task = new Task(() => TaskWork(tokenSource.Token, false));
        }

        Console.WriteLine("Canceling task");
        tokenSource.Cancel();

        try
        {
            Console.WriteLine("Starting task");
            task.Start();
            task.Wait();
        }
        catch (Exception ex)
        {
            Console.WriteLine("Exception: {0}", ex.Message);
            if (ex.InnerException != null)
            {
                Console.WriteLine("InnerException: {0}", ex.InnerException.Message);
            }
        }

        Console.WriteLine("Task.Status: {0}", task.Status);
    }

    static void ThrowIfCancellationRequestedTest(bool passTokenToConstructor)
    {
        Console.WriteLine("Creating task");
        CancellationTokenSource tokenSource = new CancellationTokenSource();
        Task task = null;
        if (passTokenToConstructor)
        {
            task = new Task(() => TaskWork(tokenSource.Token, true), tokenSource.Token);

        }
        else
        {
            task = new Task(() => TaskWork(tokenSource.Token, true));
        }

        try
        {
            Console.WriteLine("Starting task");
            task.Start();
            Thread.Sleep(100);

            Console.WriteLine("Canceling task");
            tokenSource.Cancel();
            task.Wait();
        }
        catch (Exception ex)
        {
            Console.WriteLine("Exception: {0}", ex.Message);
            if (ex.InnerException != null)
            {
                Console.WriteLine("InnerException: {0}", ex.InnerException.Message);
            }
        }

        Console.WriteLine("Task.Status: {0}", task.Status);
    }

    static void TaskWork(CancellationToken token, bool throwException)
    {
        int loopCount = 0;

        while (true)
        {
            loopCount++;
            Console.WriteLine("Task: loop count {0}", loopCount);

            token.WaitHandle.WaitOne(50);
            if (token.IsCancellationRequested)
            {
                Console.WriteLine("Task: cancellation requested");
                if (throwException)
                {
                    token.ThrowIfCancellationRequested();
                }

                break;
            }
        }
    }
}

输出:

*********************************************************************
* Start canceled task, don't pass token to constructor
*********************************************************************
Creating task
Canceling task
Starting task
Task: loop count 1
Task: cancellation requested
Task.Status: RanToCompletion

*********************************************************************
* Start canceled task, pass token to constructor
*********************************************************************
Creating task
Canceling task
Starting task
Exception: Start may not be called on a task that has completed.
Task.Status: Canceled

*********************************************************************
* Throw if cancellation requested, don't pass token to constructor
*********************************************************************
Creating task
Starting task
Task: loop count 1
Task: loop count 2
Canceling task
Task: cancellation requested
Exception: One or more errors occurred.
InnerException: The operation was canceled.
Task.Status: Faulted

*********************************************************************
* Throw if cancellation requested, pass token to constructor
*********************************************************************
Creating task
Starting task
Task: loop count 1
Task: loop count 2
Canceling task
Task: cancellation requested
Exception: One or more errors occurred.
InnerException: A task was canceled.
Task.Status: Canceled


Test Completed!!!

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