如何正确实现CancellationTokenSource的取消与释放以避免ObjectDisposedException
问题根因
你遇到的ObjectDisposedException由以下几个逻辑缺陷共同导致:
- CancellationTokenSource(以下简称cts)释放时机过早:你在
LoopAndCheckPingAsync的ContinueWith回调中直接释放cts,但此时Ping类的底层异步回调仍持有cts关联的等待句柄引用,后续回调执行时访问已释放的安全句柄就触发了异常,和你贴出的堆栈信息完全吻合。 - 静态cts字段存在线程安全隐患:如果多次调用批量Ping的启动逻辑,会出现cts实例被覆盖、新旧实例混用的问题,进一步放大了资源释放的时序问题。
- 释放逻辑覆盖不全:仅在任务正常完成时释放cts,故障、取消场景下的cts资源没有被正确释放,既存在资源泄漏风险,也可能触发其他时序问题。
ContinueWith本身的执行逻辑缺陷:没有指定任务调度器、也没有等待底层Ping资源完全清理就触发释放逻辑,进一步提前了cts的释放时机。
修复方案
1. 核心调整原则
- 优先用
async/await代替ContinueWith,简化异步流程控制,确保所有异步操作(包括底层非托管资源清理)完全完成后再执行资源释放。 - 不要将cts设为静态字段,每次执行批量Ping任务时创建独立的cts实例,避免多轮任务的资源互相干扰。
- 取消操作先调用
cts.Cancel(),等待所有异步操作响应取消完成后再执行释放,给底层资源清理留足时间。 - 用
using语句包裹cts实例,无论任务正常完成、故障、取消,都会自动触发释放,无需手动处理释放逻辑。
2. 修复后参考代码
/// 批量Ping入口方法 private async Task RunBatchPingAsync(List<string> ipList) { IsRun = true; // 每次任务创建独立cts,using会在作用域结束后自动释放 using var cts = new CancellationTokenSource(); var cancellationToken = cts.Token; // 如需支持外部主动取消任务,可将cts实例暴露给外部调用方,外部调用cts.Cancel()即可触发取消 try { await LoopAndCheckPingAsync(ipList, cancellationToken); } catch (OperationCanceledException) { Global.LOG.Log("Sonar.Start() - 批量Ping任务已取消"); } catch (Exception ex) { // 解包聚合异常获取真实错误信息 while (ex is AggregateException aggEx && aggEx.InnerException != null) ex = ex.InnerException; Global.LOG.Log($"Sonar.Start() - 执行异常:{ex.Message}"); } finally { IsRun = false; } } /// 批量Ping执行逻辑,需传入CancellationToken private async Task LoopAndCheckPingAsync(List<string> ipList, CancellationToken ct) { // 示例:每个Ping操作绑定取消令牌,且用using包裹及时释放Ping自身资源 var pingTasks = ipList.Select(ip => { using var ping = new Ping(); // 取消令牌传入Ping异步方法 return ping.SendPingAsync(ip, 1000, ct); }); await Task.WhenAll(pingTasks); // 保留你原有业务逻辑即可 }
3. 特殊场景兼容
如果确实需要保留静态cts实现全局统一取消,需要加锁保护cts的创建、取消、释放流程,避免多线程竞争:
private static readonly object _ctsLock = new object(); private static CancellationTokenSource _cts; // 全局取消方法 public static void StopBatchPing() { lock (_ctsLock) { _cts?.Cancel(); } }
内容的提问来源于stack exchange,提问作者user17327784
相关产品推荐
相关产品推荐

