非线程安全服务任务排队取消:CancellationTokenSource处置方案咨询
问题描述
- 业务需求:在工作线程执行耗时操作,结果同步到UI线程展示;依赖的非线程安全Service同一时间仅允许一个任务调用;短时间涌入大量请求时,只处理最后一个请求,其余请求直接取消/忽略(若已有任务启动,则等它执行完但丢弃结果)。
- 旧方案局限:用
BlockingCollection实现队列虽能满足需求,但每个实例都要分配专用线程,实例数量多了资源开销太大。 - 新方案思路:收到新请求就启动
Task,用资源数为1的Semaphore保证Service的独占性;将CancellationToken传入Semaphore.WaitAsync,让过时请求在获取Service前就被取消,减少等待已启动过时任务的情况。 - 当前痛点:
CancellationTokenSource的优雅销毁问题没解决,自己改进的方案还存在竞态条件。
解决方案:优雅管理CancellationTokenSource与Semaphore
核心逻辑
用原子操作存储当前有效的CancellationTokenSource,每次新请求到来时,先取消并销毁旧的Cts,再创建新的Cts替换;结合Semaphore的独占性,实现仅处理最后一个请求的目标。
具体实现
1. 定义核心变量
// 存储当前有效的取消令牌源,用原子操作保证线程安全 private CancellationTokenSource _currentCts = new CancellationTokenSource(); // 资源数为1的信号量,保证Service同一时间仅被一个任务调用 private readonly SemaphoreSlim _semaphore = new SemaphoreSlim(1, 1);
2. 请求处理逻辑
每次收到新请求时,先替换并取消旧的Cts,再启动任务执行:
public void SubmitNewRequest() { // 原子替换旧的Cts,同时取消并销毁旧实例 var oldCts = Interlocked.Exchange(ref _currentCts, new CancellationTokenSource()); oldCts.Cancel(); oldCts.Dispose(); _ = Task.Run(async () => { var token = _currentCts.Token; try { // 等待信号量,同时响应取消请求 await _semaphore.WaitAsync(token).ConfigureAwait(false); // 再次检查令牌是否已取消(防止等待期间被新请求取消) if (token.IsCancellationRequested) return; // 调用非线程安全的Service执行耗时操作 var result = await _nonThreadSafeService.DoWorkAsync().ConfigureAwait(false); // 只有当前请求未被取消时,才更新UI if (!token.IsCancellationRequested) { // 切换到UI线程更新(以WPF为例) await Application.Current.Dispatcher.InvokeAsync(() => { UpdateUiWithResult(result); }); } } catch (OperationCanceledException) { // 取消请求直接忽略,无需处理 } finally { // 无论成功/失败/取消,都必须释放信号量 _semaphore.Release(); } }); }
3. 资源优雅销毁
当组件销毁时,要清理所有资源:
public void Dispose() { _currentCts.Cancel(); _currentCts.Dispose(); _semaphore.Dispose(); }
竞态条件的解决关键
- 用
Interlocked.Exchange保证Cts替换的原子性,避免多个请求同时修改_currentCts - 获取Semaphore后二次检查令牌状态,防止等待期间新请求到来取消了当前任务
- 所有异步操作使用
ConfigureAwait(false)减少不必要的上下文切换,提升性能
压力测试验证
针对短时间大量请求场景,测试时会出现以下结果:
- 前N-1个请求会在等待Semaphore时被直接取消,无需执行耗时操作
- 若某个请求已进入Service执行,会完成但结果被丢弃(因为新请求已替换Cts,令牌已取消)
- 仅最后一个请求的结果会同步到UI展示
注意事项
- 无论任务是否被取消,必须在finally块中释放Semaphore,避免Service被永久占用
- UI更新必须切换到UI线程执行,不同UI框架对应不同的切换方式(如WinForms用
Control.Invoke) - 组件销毁时务必调用Dispose方法,释放所有资源避免内存泄漏
内容的提问来源于stack exchange,提问作者creativergk
相关产品推荐
相关产品推荐

