如何实现取消并正确处置 CancellationTokenSource

Posted

技术标签:

【中文标题】如何实现取消并正确处置 CancellationTokenSource【英文标题】:How to implement cancellation and dispose a CancellationTokenSource correctly 【发布时间】:2021-12-02 15:42:45 【问题描述】:

这是我用来 ping IP 地址列表的代码。它工作正常,除了今天我收到了一个致命的未处理异常! - System.ObjectDisposedException

private static CancellationTokenSource cts = new CancellationTokenSource();
private static CancellationToken ct;

// Source per cancellation Token
ct = cts.Token;

IsRun = true;
try

    LoopAndCheckPingAsync(AddressList.Select(a => a.IP).ToList()).ContinueWith((t) =>
    
        if (t.IsFaulted)
        
            Exception ex = t.Exception;
            while (ex is AggregateException && ex.InnerException != null)
                ex = ex.InnerException;
            Global.LOG.Log("Sonar.Start() - ContinueWith Faulted:" + ex.Message);
        
        else
        
            // Cancellation tokek
            if (cts != null)
            
                cts.Dispose();          
            
        
    );

catch (Exception ex)

    Global.LOG.Log("Sonar.Start() - Exc:" + ex.Message);

由于我无法复制错误,我的怀疑与 CancellationTokenSource 的 Disponse 方法有关。有什么想法可以正确处理 CancellationTokenSource?

我获取了事件查看器详细信息条目:

Informazioni sull'eccezione: System.ObjectDisposedException
   in System.Runtime.InteropServices.SafeHandle.DangerousAddRef(Boolean ByRef)
   in System.StubHelpers.StubHelpers.SafeHandleAddRef(System.Runtime.InteropServices.SafeHandle, Boolean ByRef)
   in Microsoft.Win32.Win32Native.SetEvent(Microsoft.Win32.SafeHandles.SafeWaitHandle)
   in System.Threading.EventWaitHandle.Set()
   in System.Net.NetworkInformation.Ping.set_InAsyncCall(Boolean)
   in System.Net.NetworkInformation.Ping.Finish(Boolean)
   in System.Net.NetworkInformation.Ping.PingCallback(System.Object, Boolean)
   in System.Threading._ThreadPoolWaitOrTimerCallback.WaitOrTimerCallback_Context(System.Object, Boolean)
   in System.Threading._ThreadPoolWaitOrTimerCallback.WaitOrTimerCallback_Context_f(System.Object)
   in System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean)
   in System.Threading.ExecutionContext.Run(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object, Boolean)
   in System.Threading._ThreadPoolWaitOrTimerCallback.PerformWaitOrTimerCallback(System.Object, Boolean)

【问题讨论】:

你的代码看起来也坏了。如果 LoopAndCheckPingAsync 返回一个任务(方法名称暗示了这一点),你必须等待这个方法:await LoopAndCheckPingAsync()。不要使用 Task.ContinueWith。由于您正在等待该方法,因此等待后面的代码将自动视为继续。 您还必须在记录其消息后抛出原始异常。 @BionicCode 好的,我明白了。我使用await 更改了 LoopAndCheckPingAsync 方法,但我不知道如何处理 cancelToken 如果这是真实/完整的代码,您也必须删除完整的延续。 await 将正确处理异常,现有的 catch 将记录它们。继续是完全多余的。然后像我在回答中建议的那样控制 CancellationTokenSource 。我想知道您为什么不将 CancellationToken 传递给 LoopAndCheckPingAsync 方法?它应该有一个 CancellationToken 参数而不是一个静态引用。 不错的方法,我会尝试的。非常感谢@BionicCode 【参考方案1】:

无法判断错误源自您发布的代码的位置。通常,您必须检查接收消息(调用堆栈)以了解异常触发的确切位置。

一旦请求取消或可取消的操作已完成,请致电 Dispose。当您访问CancellationTokenSource 或其CancellationToken 实例的变异成员时,当CancellationTokenSource 暴露时,将引发异常。就像在已处理的实例上调用 Cancel 或在调用 Dispose 之后尝试获取对关联的 CancellationToken 的引用一样。您必须确保没有代码访问已处置的实例。

您可以通过在处置时将 CancellationTokenSource 属性设置为 null 并在访问 CancellationTokenSource 之前添加 null 检查来执行此操作。您必须谨慎控制CancellationTokenSource 的生命周期。

下面的例子展示了如何控制CancellationTokenSource的生命周期和防止非法引用被释放的实例:

private CancellationTokenSource CancellationtokenSource  get; set; 

private void CancelCancellableOperation_OnClick(object sender, EventArgs e)

  // Check for null to avoid an ObjectDisposedException 
  // (or a  NullReferenceException in particular) exception.
  // The implemented pattern sets the property to null immediately after disposal (not thread-safe).
  this.CancellationTokenSource?.Cancel();


// Start scope of CancellationTokenSource. 
// Lifetime is managed by a try-catch-finally block and the use of 
// CancellationToken.ThrowIfCancellationRequested 
// to forcefully enter the try-catch-finally block on cancellation.
private async Task DoWorkAsync()

  this.CancellationTokenSource = new CancellationTokenSource();
  try
  
    await CancellableOperationAsync(this.CancellationTokenSource.Token);
  
  catch (OperationCanceledException)
  
    // Do some cleanup or rollback. 
    // At this point the CancellationTokenSource is still not disposed.
  
  finally 
  
    // Invalidate CancellationTokenSource property to raise an NullReferenceException exception 
    // to indicate that thet access ocurred uncontrolled and requires a fix.
    // Create a local copy of the property to avoid race conditions.
    var cancellationTokenSource = this.CancellationTokenSource;
    this.CancellationTokenSource = null;

    // Dispose after cancellation 
    // or cancellable operations are completed
    cancellationTokenSource.Dispose();
  


private async Task CancellableOperationAsync(CancellationToken cancellationToken)

  // Guarantee that CancellationTokenSource is never disposed before
  // CancellationTokenSource.Cancel was called or the cancellable operation has completed

  // Do something
  while (true)
  
    await Task.Delay(TimeSpan.FromSeconds(10));

    // Add null check if you can't guard against premature disposal
    cancellationToken?.ThrowIfCancellationRequested();
  

【讨论】:

我真的很喜欢你提出的答案的结构。我只是想知道:为什么您首先将属性设置为 null ,然后使用另一个变量来处理?为什么不先对属性调用 .Dispose ,然后将其设置为 null 呢? 它是可选的并且针对并发环境。通过这种方式,我们可以避免竞争条件,以防其他线程访问令牌源属性同时已经处置它。您仍然可以多次取消它,因此它不提供完整的线程安全性。为了实现这一点,我们当然需要同步对令牌源的访问。它只是使令牌无效,其他调用者只需要关心空引用并且不会遇到处理 ObjectDisposedException 的情况。它不是取消模式的重要组成部分。 非常感谢!如果我们的应用程序中没有多线程,那么我们就不需要使用附加变量,对吗?相反,我们可以直接处置该属性,然后将其设置为 null? 是的,完全正确。就是这样。 非常感谢!【参考方案2】:

解决您的问题的最简单方法是处理取消令牌源。 根据 MS 和一些 posts 的说法,只有当它是 Linked cancellation token source 或(这里我不完全确定)如果令牌的 Register 方法分配了某些东西时,才需要处理取消令牌源。

【讨论】:

以上是关于如何实现取消并正确处置 CancellationTokenSource的主要内容,如果未能解决你的问题,请参考以下文章

如何“取消处置”动画控制器以供重用?

检查物品是不是已被处置的正确方法

处置 WCF 代理的正确方法是啥?

如何从内容处置中获取文件名

处置(取消)可观察的。 SubscribeOn 和 observeOn 不同的调度器

如何正确取消请求?