任务构造器中的取消令牌:为什么?


223

某些System.Threading.Tasks.Task构造函数将a CancellationToken作为参数:

CancellationTokenSource source = new CancellationTokenSource();
Task t = new Task (/* method */, source.Token);

令我感到困惑的是,没有办法从方法主体内部真正获取传入的令牌(例如,没有类似的东西Task.CurrentTask.CancellationToken)。令牌必须通过某种其他机制提供,例如状态对象或在lambda中捕获。

那么在构造函数中提供取消令牌有什么目的呢?

Answers:


254

传递CancellationTokenTask构造函数与任务关联起来。

引用来自MSDN的Stephen Toub的答案

这有两个主要好处:

  1. 如果令牌在Task开始执行之前已请求取消,Task则不会执行。而不是过渡到 Running,它会立即过渡到Canceled。这样就避免了在无论如何运行时都将其取消的任务。
  2. 如果任务的主体还在监视取消令牌,并抛出OperationCanceledException包含该令牌的令牌(ThrowIfCancellationRequested确实如此),那么当任务看到时OperationCanceledException,它将检查OperationCanceledException的令牌是否与任务的令牌匹配。如果确实如此,则将该异常视为对协作取消的确认,并Task转换为Canceled 状态(而不是Faulted状态)。

2
TPL的考虑周到。
上校恐慌

1
我认为好处1适用于将取消令牌传递给Parallel.ForParallel.ForEach
上校恐慌

27

构造函数在内部使用令牌进行取消处理。如果您的代码想要访问令牌,则您有责任将其传递给自己。我强烈建议阅读CodePlex上的《与Microsoft .NET并行编程》一书

本书中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 2011年

2
@sergdev:您传递令牌以将其注册到任务和调度程序。不传递并使用它将是未定义的行为。
2011年

3
@sergdev:经过测试:未将令牌作为参数传递时,myTask.IsCanceled和myTask.Status不相同。状态将失败而不是取消。但是,例外是相同的:在两种情况下都是OperationCanceledException。
Olivier de Rivoyre,2015年

2
如果我不打电话token.ThrowIfCancellationRequested();怎么办?在我的测试中,行为是相同的。有任何想法吗?
machinarium 2015年

1
@CobaltBlue:when cts.Cancel() is called the Task is going to get canceled and end, no matter what you do不。如果任务在开始之前被取消,则被取消。如果Task的主体根本不检查任何令牌,它将运行至完成,从而导致RanToCompletion状态。如果主体抛出OperationCancelledException,例如by ThrowIfCancellationRequested,则Task将检查Exception的CancellationToken是否与与Task关联的那个相同。如果是,则任务被取消。如果不是,那就是Faulted
Wolfzoon

7

取消并非像许多人想象的那样简单。在msdn上的此博客文章中解释了一些细微之处:

例如:

在Parallel Extensions和其他系统中的某些情况下,有必要唤醒阻塞的方法,其原因并非由用户明确取消引起。例如,如果一个线程blockingCollection.Take()由于该集合为空而被阻塞,而另一个线程随后调用 blockingCollection.CompleteAdding(),则第一个调用应唤醒并抛出,InvalidOperationException以表示不正确的用法。

并行扩展中的取消


4

这是一个示例,展示了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 Done!!!");
        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 Done!!!
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.