参考:Taskcancellation in C# and things you should know about it 微软官方有一篇内容差不多的:Cancellation in Managed Threads

取消一个 Task 的步骤:

  1. 创建 CancellationToken(下文中以 token 表示),并将其传入 Task
  2. Task 内时刻监视 token 的取消请求(CancellationRequested)
  3. Task 检测到取消请求就做出响应
    1. 在响应的最后 token.ThrowIfCancellationRequested() 抛出 OperationCanceledException
  4. 调用线程对任务取消进行响应
    1. 使用 try catch 捕获 OperationCanceledException

.NET 给我们提供了两个类用于取消 Task:

  • CancellationTokenSource:负责创建 token 实例和发送取消请求给从它创建的所有 token 实例
  • CancellationToken:传播取消请求

如何传递 token

下面先展示最简单(也是最没用)的方法。

  1. CancellationTokenSource tokenSource = new CancellationTokenSource();
  2. CancellationToken token = tokenSource.Token;
  3. tokenSource.Cancel();
  4. Task.Run(() => Console.WriteLine("Hello from Task"), token);

这种情况 Task.Run 只会在开始任务前检查一下 token。

如果希望在 Task 执行过程中取消它,我们需要将 token 彻底传入 Task。

两种方法:

  1. 使用全局变量或变量获取,使得 token 在 Task 内也可见

    1. var token = tokenSource.Token;
    2. Task.Run(() =>
    3. {
    4. var capturedToken = token;
    5. ...
    6. }, token);
  2. 将 token 作为状态对象传递,并在 Task 内再转换为 token

    1. var token = tokenSource.Token;
    2. Task.Factory.StartNew(stateObject =>
    3. {
    4. var castedToken = (CancellationToken) stateObject;
    5. ...
    6. }, token, token);

如何监控 token

现在我们在任务内部可以读取 token 了,接下来要关心的就是如何监控 token 的状态。

三种方式:

  1. By polling:轮询 token.IsCancellationRequested

    1. Task.Run(() =>
    2. {
    3. //Polling boolean property
    4. while (!token.IsCancellationRequested) {
    5. //Do some work
    6. }
    7. //Release resources and exit
    8. }, token);
  2. Callback registration:注册一旦取消请求被发出就执行的回调方法

    1. Task.Run(async () =>
    2. {
    3. var webClient = newWebClient();
    4. //Registering callback that would cancel downloading
    5. token.Register(() => webClient.CancelAsync());
    6. var data = await webClient.DownloadDataTaskAsync("http://www.google.com/");
    7. }, token);
  3. 等待 token.WaitHandle

    1. WaitHandle 将在 token 取消时发出信号
    2. WaitHandle.WaitAny 可以等待并接收它发出的信号
      1. var autoResetEvent = newAutoResetEvent(false);
      2. //Some code...
      3. Task.Run(() =>
      4. {
      5. var handleArray = new[] {autoResetEvent, token.WaitHandle};
      6. //Waiting on wait handle to signal first
      7. var finishedId = WaitHandle.WaitAny(handleArray);
      8. if (finishedId == Array.IndexOf(handleArray, token.WaitHandle)){
      9. //If token wait handle was first to signal then exit
      10. return;
      11. }
      12. //Continue working...
      13. }, token);

注:可以通过创建 LinkedTokenSource 同时监控多个 token。

  1. CancellationTokenSource linkedToukenSource = CancellationTokenSource.CreateLinkedTokenSource(firstToken, secondToken);
  2. CancellationToken lindkedToken = linkedToukenSource.Token;

如何取消 Task

知道如何传递和监控 token 后,剩下的就是如何发送取消请求彻底取消 Task 了。
几种发送 cancellation 请求的方法:

  1. //#1 在一定时间后发送取消请求
  2. tokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(2));
  3. //#2 在一定时间后发送取消请求
  4. tokenSource.CancelAfter(TimeSpan.FromSeconds(2));
  5. //#3 直接发送取消请求
  6. tokenSource.Cancel();
  7. //or
  8. tokenSource.Cancel(true);

当我们取消任务时,它会隐式调用当前线程所有注册了的回调函数。如果回调函数抛出了异常,这些异常将被包装为一个 AggregateException(异常集合,里面有许多子异常)并将其抛给调用者。

因为回调函数是依次执行的,所以通过 Cancel 可选的 bool 参数,你可以指定这些回调函数遇到异常时的行为。

  • true:异常马上抛出,后面的回调函数不再执行
  • false:所有注册了的回调函数都执行,它们抛出的异常包装为一个AggregateException


    当在 task 内检测到 cancellation request 后,通常的做法是抛出 OperationCanceledException,让 task 进入 canceled 状态。

    1. Task.Run(() =>
    2. {
    3. while (true)
    4. {
    5. //Do some work
    6. if (token.IsCancellationRequested)
    7. {
    8. token.ThrowIfCancellationRequested();
    9. }
    10. }
    11. }, token);