处理取消令牌源的正确模式

Correct pattern to dispose of cancellation token source

考虑这样一个场景,您有一些异步工作需要完成,您可以 运行 它处于即发即弃模式。此异步工作能够侦听取消,因此您将取消令牌传递给它以便能够取消它。

在给定的时刻,我们可以决定请求取消正在进行的 activity,方法是使用我们从中获取取消令牌的取消令牌源对象。

因为取消令牌源实现了 IDisposable,我们应该在完成后调用它的 Dispose 方法。这个问题的重点是准确地确定何时你完成了给定的取消令牌源。

假设您决定通过在取消标记源上调用 Cancel 方法来取消正在进行的工作:是否有必要在调用 [ 之前等待正在进行的操作完成=13=] ?

换句话说,我应该这样做吗:

class Program 
{
  static void Main(string[] args) 
  {
    var cts = new CancellationTokenSource();
    var token = cts.Token;

    DoSomeAsyncWork(token); // starts the asynchronous work in a fire and forget manner

    // do some other stuff here 

    cts.Cancel();
    cts.Dispose(); // I call Dispose immediately after cancelling without waiting for the completion of ongoing work listening to the cancellation requests via the token

    // do some other stuff here not involving the cancellation token source because it's disposed
  }

  async static Task DoSomeAsyncWork(CancellationToken token) 
  {
     await Task.Delay(5000, token).ConfigureAwait(false);
  }
}

或者这样:

class Program 
{
  static async Task Main(string[] args) 
  {
    var cts = new CancellationTokenSource();
    var token = cts.Token;

    var task = DoSomeAsyncWork(token); // starts the asynchronous work in a fire and forget manner

    // do some other stuff here 

    cts.Cancel();

    try 
    {
      await task.ConfigureAwait(false);
    }
    catch(OperationCanceledException) 
    {
      // this exception is raised by design by the cancellation
    }
    catch (Exception) 
    {
      // an error has occurred in the asynchronous work before cancellation was requested
    }

    cts.Dispose(); // I call Dispose only when I'm sure that the ongoing work has completed

    // do some other stuff here not involving the cancellation token source because it's disposed
  }

  async static Task DoSomeAsyncWork(CancellationToken token) 
  {
     await Task.Delay(5000, token).ConfigureAwait(false);
  }
}

其他详细信息:我所指的代码是在 ASP.NET 核心 2.2 Web 应用程序中编写的,这里我使用控制台应用程序场景只是为了简化我的示例。

我在 Whosebug 上发现了类似的问题,询问是否需要处理取消令牌源对象。一些答案表明在某些情况下并不真正需要处理这个对象。

我对整个 IDisposable 主题的态度是,我总是倾向于遵守 class 的公开合同,换句话说,如果一个对象声称是一次性的,我更喜欢总是调用Dispose 完成后。我不喜欢根据 class 的实现细节来猜测是否真的需要调用 dispose 的想法,这些细节可能会在未来的版本中以未记录的方式发生变化。

正确的做法是 - 在确定任务被取消后处理CancellationTokenSourceCancellationToken 依赖 CancellationTokenSource 的信息才能正常运行。虽然当前实现 CancellationToken 的编写方式即使在创建它的 CTS 被处置时不会抛出异常也仍然有效,但它可能无法正常运行或始终按预期运行。

要确保 CTS(CancellationTokenSource) associated with a fire-and-forget Task will be eventually disposed, you should attach a continuation to the task, and dispose the CTS from inside the continuation. This creates a problem though, because another thread could call the Cancel method while the object is in the midst of its disposal, and according to the documentation Dispose 方法不是线程安全的:

All public and protected members of CancellationTokenSource are thread-safe and may be used concurrently from multiple threads, with the exception of Dispose(), which must only be used when all other operations on the CancellationTokenSource object have completed.

因此从两个不同的线程同时调用 CancelDispose 而不同步不是一个选项。这只剩下一个选项可用:在 CTS class 的所有 public 成员周围添加一层同步。这不是一个令人满意的选择,原因如下:

  1. 你必须写线程安全的包装器class(写代码)
  2. 每次启动可取消的即发即弃任务时都必须使用它(多写代码)
  3. 导致同步的性能损失
  4. 导致附加延续的性能损失
  5. 不得不维护一个变得更复杂、更容易出错的系统
  6. 必须解决为什么 class 一开始就没有设计成线程安全的哲学问题

因此,我的建议是采用替代方案,即不处置 CTS,仅在您无法等待其相关任务完成的情况下使用。换句话说,如果无法将使用 CTS 的代码包含在文档的 using statement, just let the garbage collector to do the reclaiming of the reserved resources. This means that you'll have to disobey this 部分中:

Always call Dispose before you release your last reference to the CancellationTokenSource. Otherwise, the resources it is using will not be freed until the garbage collector calls the CancellationTokenSource object's Finalize method.

...和this:

The CancellationTokenSource class implements the IDisposable interface. You should be sure to call the CancellationTokenSource.Dispose method when you have finished using the cancellation token source to free any unmanaged resources it holds.

如果这让您觉得有点脏,那么您并不孤单。如果您认为 Task class implements the IDisposable interface too, but disposing task instances is not required.

与任何 IDisposable 一样,您会在使用完资源后将其处置。这是 IDisposable 的硬性规定,我还没有遇到过不是这种情况的情况,但我当然愿意学习 ;)。

CancellationTokenSource 的情况下,这意味着当对象本身和 Token 属性 都不再使用时,您将释放源。 (我刚刚为这个声明打开了一个来源,但可惜我分心并以某种方式丢失了它)

所以你处理任务时不再使用CancellationToken。在你的情况下,第二个选项,因为你确定没有任务使用令牌。

编辑;除此之外,最好的做法是将实现一次性的任何属性设置为 null。在这种情况下,因为您只有局部变量,所以这无关紧要,但是当您将令牌源作为字段或其他内容时,请确保将字段设置为 null,以便没有对令牌源的引用。