You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

非线程安全服务任务排队取消: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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 20:52:52