【问题标题】:When to dispose CancellationTokenSource?何时处置 CancellationTokenSource?
【发布时间】:2011-08-05 17:51:03
【问题描述】:

CancellationTokenSource 类是一次性的。快速浏览一下 Reflector 可以证明使用了 KernelEvent,这是一个(很可能)非托管资源。 由于CancellationTokenSource 没有终结器,如果我们不处理它,GC 就不会处理它。

另一方面,如果您查看 MSDN 文章 Cancellation in Managed Threads 中列出的示例,则只有一个代码 sn-p 处理令牌。

在代码中处理它的正确方法是什么?

  1. 如果您不等待,则无法使用using 包装开始并行任务的代码。只有在您不等待的情况下取消才有意义。
  2. 当然,您可以通过Dispose 调用在任务中添加ContinueWith,但这是要走的路吗?
  3. 关于可取消的 PLINQ 查询,它不会同步回来,但只是在最后做一些事情?比如.ForAll(x => Console.Write(x))
  4. 它可以重复使用吗?是否可以将同一个令牌用于多个调用,然后将其与宿主组件一起处置,比如 UI 控件?

因为它没有类似 Reset 的方法来清理 IsCancelRequestedToken 字段,所以我认为它不可重用,因此每次启动任务(或 PLINQ 查询)时,您都应该创建一个新的。这是真的吗?如果是,我的问题是在众多 CancellationTokenSource 实例上处理 Dispose 的正确和推荐策略是什么?

【问题讨论】:

    标签: c# c#-4.0 task-parallel-library plinq parallel-extensions


    【解决方案1】:

    谈到是否真的有必要在 CancellationTokenSource 上调用 Dispose... 我的项目中出现了内存泄漏,结果发现问题是 CancellationTokenSource

    我的项目有一项服务,它不断读取数据库并触发不同的任务,并且我正在将链接的取消令牌传递给我的工作人员,因此即使在他们完成数据处理之后,取消令牌也没有被释放,这导致了内存泄漏。

    MSDN Cancellation in Managed Threads 明确说明:

    请注意,完成后,您必须在链接的令牌源上调用 Dispose。如需更完整的示例,请参阅How to: Listen for Multiple Cancellation Requests

    我在实现中使用了ContinueWith

    【讨论】:

    • 这是 Bryan Crosby 当前接受的答案中的一个重要遗漏 - 如果您创建 linked CTS,则存在内存泄漏的风险。该场景非常类似于永远不会取消注册的事件处理程序。
    • 由于同样的问题,我发生了泄漏。使用分析器,我可以看到回调注册持有对链接 CTS 实例的引用。检查 CTS Dispose 实现 here 的代码非常有见地,并强调了 @SørenBoisen 与事件处理程序注册泄漏的比较。
    • 以上评论反映了讨论状态,@Bryan Crosby 的另一个回答被接受了。
    • 2020年的文档中明确写着:Important: 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. - docs.microsoft.com/en-us/dotnet/standard/threading/…
    【解决方案2】:

    我认为当前的任何答案都不令人满意。经过研究,我发现了 Stephen Toub (reference) 的回复:

    这取决于。 在 .NET 4 中,CTS.Dispose 有两个主要用途。如果 CancellationToken 的 WaitHandle 已被访问(因此懒惰 分配它),Dispose 将处理该句柄。此外,如果 CTS 是通过 CreateLinkedTokenSource 方法 Dispose 创建的 将取消 CTS 与其链接到的令牌的链接。在 .NET 4.5 中, Dispose 还有一个目的,就是如果 CTS 使用 Timer 在幕后(例如 CancelAfter 被调用),计时器将是 弃置。

    很少使用 CancellationToken.WaitHandle, 所以在它之后清理通常不是使用 Dispose 的好理由。 但是,如果您使用 CreateLinkedTokenSource 创建 CTS,或者 如果您使用 CTS 的计时器功能,它可能会更有影响力 使用 Dispose。

    我认为粗体部分是重要的部分。他使用“更有影响力”,这让它有点模糊。我将其解释为应该在这些情况下调用Dispose,否则不需要使用Dispose

    【讨论】:

    • 更具影响力意味着将子 CTS 添加到父 CTS。如果你不处置孩子,如果父母长寿,就会有泄漏。所以处理链接的那些是至关重要的。
    • 而且你永远不知道你在某处传递的令牌何时会被链接到
    【解决方案3】:

    您应该始终处置CancellationTokenSource

    如何处置它完全取决于场景。您提出了几种不同的方案。

    1. using 仅在您将CancellationTokenSource 用于您正在等待的某些并行工作时才有效。如果这是您的方案,那就太好了,这是最简单的方法。

    2. 使用任务时,请按照您的指示使用ContinueWith 任务来处理CancellationTokenSource

    3. 对于 plinq,您可以使用 using,因为您正在并行运行它,但要等待所有并行运行的工作人员完成。

    4. 对于 UI,您可以为每个未绑定到单个取消触发器的可取消操作创建一个新的 CancellationTokenSource。维护一个List<IDisposable> 并将每个源添加到列表中,在您的组件被释放时将它们全部释放。

    5. 对于线程,创建一个新线程来连接所有工作线程,并在所有工作线程完成后关闭单个源。见CancellationTokenSource, When to dispose?

    总有办法。 IDisposable 实例应始终被处置。示例通常不会,因为它们要么是显示核心用法的快速示例,要么是因为添加正在演示的类的所有方面对于示例来说过于复杂。样本只是一个样本,不一定(甚至通常)生产质量代码。并非所有示例都可以按原样复制到生产代码中。

    【讨论】:

    • 对于第 2 点,您有什么理由不能在任务上使用 await 并在等待之后的代码中处理 CancellationTokenSource?
    • 有一些警告。如果 CTS 在您 await 操作时被取消,您可能会因 OperationCanceledException 而恢复。然后您可以致电Dispose()。但是,如果有操作仍在运行并使用相应的CancellationToken,则即使源已处置,该令牌仍会将CanBeCanceled 报告为true。如果他们尝试注册取消回调,BOOM!ObjectDisposedException。在成功完成操作后调用Dispose() 是足够安全的。当你真正需要取消某些东西时,它变得真的很棘手
    • 由于 Mike Strobel 给出的原因而被否决 - 由于 CTS 和 Task 的异步性质,在处理 CTS 和 Task 时,强制规则始终调用 Dispose 会使您陷入困境。规则应改为:始终处置 linked 令牌来源。
    • 您的链接指向已删除的答案。
    • 有关@MikeStrobel 描述的条件的更多信息,请参阅this concise summary 各种CTS 危险。此外,作为记录,如果此类问题适用于您的代码库,但您仍然坚持处置每个 CTS,则必要的方法/修复将是在处置 CTS 之前手动取消注册链接并分离任何计时器。但请注意,您可能是in for it
    【解决方案4】:

    我在ILSpy 中查看了CancellationTokenSource,但我只能找到m_KernelEvent,它实际上是ManualResetEvent,它是WaitHandle 对象的包装类。这应该由 GC 妥善处理。

    【讨论】:

    • 我有同样的感觉,GC 会清理所有这些。我会尝试验证这一点。为什么微软在这种情况下实施 dispose?摆脱事件回调并避免传播到第二代 GC。在这种情况下,调用 Dispose 是可选的 - 如果可以,请调用它,如果不能忽略它。不是我认为的最佳方式。
    • 我已经调查过这个问题。 CancellationTokenSource 被垃圾收集。您可能会帮助处理在 GEN 1 GC 中执行此操作。接受。
    • 我独立进行了同样的调查并得出了相同的结论:如果可以轻松处理,但不要担心在罕见但并非闻所未闻的情况下尝试这样做您已将 CancellationToken 发送到 boondocks 并且不想等待他们写回明信片告诉您他们已经完成了它。由于 CancellationToken 的用途,这种情况会时不时发生,我保证,这真的没问题。
    • 我的上述评论不适用于链接的令牌来源;我无法证明不处理这些是可以的,而这个线程和 MSDN 中的智慧表明它可能不是。
    【解决方案5】:

    这个答案仍在谷歌搜索中出现,我相信投票的答案并没有给出完整的故事。在查看了source codeCancellationTokenSource (CTS) 和CancellationToken (CT) 之后,我相信对于大多数用例来说,以下代码序列是可以的:

    if (cancelTokenSource != null)
    {
        cancelTokenSource.Cancel();
        cancelTokenSource.Dispose();
        cancelTokenSource = null;
    }
    

    上面提到的m_kernelHandle 内部字段是支持CTS 和CT 类中WaitHandle 属性的同步对象。仅当您访问该属性时才会实例化它。因此,除非您在 Task 调用 dispose 中使用 WaitHandle 进行一些老式线程同步,否则将无效。

    当然,如果您正在使用它,您应该按照上面其他答案的建议进行操作,并延迟调用 Dispose 直到使用句柄的任何 WaitHandle 操作完成,因为,如在Windows API documentation for WaitHandle中有描述,结果未定义。

    【讨论】:

    • MSDN 文章 Cancellation in Managed Threads 指出:“侦听器通过轮询、回调或等待句柄监视令牌的 IsCancellationRequested 属性的值。”换句话说:使用等待句柄的可能不是(即发出异步请求的人),它可能是侦听器(即应答请求的人)。这意味着您作为负责处理的人,实际上无法控制是否使用等待句柄。
    • 根据 MSDN,注册的回调有异常会导致 .Cancel 抛出。如果发生这种情况,您的代码将不会调用 .Dispose()。回调应该小心不要这样做,但它可能会发生。
    【解决方案6】:

    自从我提出这个问题并得到了许多有用的答案以来已经有很长时间了,但我遇到了一个与此相关的有趣问题,并认为我会在这里发布它作为另一种答案:

    只有当您确定没有人会尝试获取 CTS 的 Token 属性时,您才应该调用 CancellationTokenSource.Dispose()。否则,您应该调用Dispose(),因为它会产生竞争条件。例如,请参见此处:

    https://github.com/aspnet/AspNetKatana/issues/108

    在此问题的修复中,之前执行 cts.Cancel(); cts.Dispose(); 的代码被编辑为仅执行 cts.Cancel();,因为任何不幸的人都试图获取取消令牌以观察其取消状态之后 em> Dispose 已被调用,不幸的是,除了他们计划的 OperationCanceledException 之外,还需要处理 ObjectDisposedException

    Tratcher 提出了与此修复相关的另一个关键观察:“只有不会被取消的令牌才需要处置,因为取消会进行所有相同的清理。” 即只做Cancel() 而不是处理就足够了!

    【讨论】:

      【解决方案7】:

      我创建了一个线程安全的类,它将CancellationTokenSource 绑定到Task,并保证CancellationTokenSource 将在其关联的Task 完成时被释放。它使用锁来确保CancellationTokenSource 在处理期间或处理后不会被取消。这是为了遵守documentation,它指出:

      只有在对CancellationTokenSource 对象的所有其他操作都完成后,才能使用Dispose 方法。

      还有also:

      Dispose 方法使CancellationTokenSource 处于不可用状态。

      这是课程:

      public class CancelableExecution
      {
          private readonly bool _allowConcurrency;
          private Operation _activeOperation;
      
          // Represents a cancelable operation that signals its completion when disposed
          private class Operation : IDisposable
          {
              private readonly CancellationTokenSource _cts;
              private readonly TaskCompletionSource<bool> _completionSource;
              private bool _disposed;
      
              public Task Completion => _completionSource.Task; // Never fails
      
              public Operation(CancellationTokenSource cts)
              {
                  _cts = cts;
                  _completionSource = new TaskCompletionSource<bool>(
                      TaskCreationOptions.RunContinuationsAsynchronously);
              }
      
              public void Cancel() { lock (this) if (!_disposed) _cts.Cancel(); }
      
              void IDisposable.Dispose() // It is disposed once and only once
              {
                  try { lock (this) { _cts.Dispose(); _disposed = true; } }
                  finally { _completionSource.SetResult(true); }
              }
          }
      
          public CancelableExecution(bool allowConcurrency)
          {
              _allowConcurrency = allowConcurrency;
          }
          public CancelableExecution() : this(false) { }
      
          public bool IsRunning => Volatile.Read(ref _activeOperation) != null;
      
          public async Task<TResult> RunAsync<TResult>(
              Func<CancellationToken, Task<TResult>> action,
              CancellationToken extraToken = default)
          {
              if (action == null) throw new ArgumentNullException(nameof(action));
              var cts = CancellationTokenSource.CreateLinkedTokenSource(extraToken);
              using (var operation = new Operation(cts))
              {
                  // Set this as the active operation
                  var oldOperation = Interlocked.Exchange(ref _activeOperation, operation);
                  try
                  {
                      if (oldOperation != null && !_allowConcurrency)
                      {
                          oldOperation.Cancel();
                          await oldOperation.Completion; // Continue on captured context
                          // The Completion never fails
                      }
                      cts.Token.ThrowIfCancellationRequested();
                      var task = action(cts.Token); // Invoke on the initial context
                      return await task.ConfigureAwait(false);
                  }
                  finally
                  {
                      // If this is still the active operation, set it back to null
                      Interlocked.CompareExchange(ref _activeOperation, null, operation);
                  }
              }
              // The cts is disposed along with the operation
          }
      
          public Task RunAsync(Func<CancellationToken, Task> action,
              CancellationToken extraToken = default)
          {
              if (action == null) throw new ArgumentNullException(nameof(action));
              return RunAsync<object>(async ct =>
              {
                  await action(ct).ConfigureAwait(false);
                  return null;
              }, extraToken);
          }
      
          public Task CancelAsync()
          {
              var operation = Volatile.Read(ref _activeOperation);
              if (operation == null) return Task.CompletedTask;
              operation.Cancel();
              return operation.Completion;
          }
      
          public bool Cancel() => CancelAsync() != Task.CompletedTask;
      }
      

      CancelableExecution 类的主要方法是RunAsyncCancel。默认情况下不允许并发操作,这意味着第二次调用RunAsync 将静默取消并等待上一个操作完成(如果它仍在运行),然后再开始新操作。

      此类可用于任何类型的应用程序。虽然它的主要用途是在 UI 应用程序中,在带有用于启动和取消异步操作的按钮的表单内部,或者在每次更改其选定项目时取消和重新启动操作的列表框。这是第一种情况的示例:

      private readonly CancelableExecution _cancelableExecution = new CancelableExecution();
      
      private async void btnExecute_Click(object sender, EventArgs e)
      {
          string result;
          try
          {
              Cursor = Cursors.WaitCursor;
              btnExecute.Enabled = false;
              btnCancel.Enabled = true;
              result = await _cancelableExecution.RunAsync(async ct =>
              {
                  await Task.Delay(3000, ct); // Simulate some cancelable I/O operation
                  return "Hello!";
              });
          }
          catch (OperationCanceledException)
          {
              return;
          }
          finally
          {
              btnExecute.Enabled = true;
              btnCancel.Enabled = false;
              Cursor = Cursors.Default;
          }
          this.Text += result;
      }
      
      private void btnCancel_Click(object sender, EventArgs e)
      {
          _cancelableExecution.Cancel();
      }
      

      RunAsync 方法接受一个额外的CancellationToken 作为参数,它链接到内部创建的CancellationTokenSource。提供此可选令牌在高级场景中可能很有用。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多