如何实现取消并正确处置 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的主要内容,如果未能解决你的问题,请参考以下文章