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

C#线程取消异常:已取消的锁续约线程仍持续执行

后台线程停止后KeepRenewingLock线程持续运行的原因及修复

问题背景

生产环境出现异常:循环执行的后台线程已停止,但关联的KeepRenewingLock线程仍在运行,锁项持续被续约。原预期通过finally块中的leaseCancellation?.Cancel()取消该线程,但未生效。触发场景为IBackgroundJob实现因Http超时(超过100秒)抛出TaskCancelledException,代码退出内层while循环后,锁续约仍未停止。

核心原因分析

1. KeepRenewingLock方法未及时响应取消令牌

KeepRenewingLock是同步void方法,内部循环依赖!cancellationToken.IsCancellationRequested判断是否继续,但存在两个关键问题:

  • 如果循环内的延迟使用Thread.Sleep而非支持取消的Task.Delay,线程会进入无响应睡眠,直到睡眠结束才会检查取消状态,这段时间锁仍会被续约;
  • 若锁续约的业务代码执行时间较长,即使取消令牌已触发,也需等待当前续约操作完成才会退出循环,导致锁续约持续。

2. 内层while循环退出条件存在冗余逻辑

内层循环条件为!cancellationToken.IsCancellationRequested && !maintainLockTask.IsCompleted,但maintainLockTask只有在KeepRenewingLock方法执行完毕后才会标记为完成,而KeepRenewingLock是无限循环(除非被取消),因此!maintainLockTask.IsCompleted始终为真,该条件等价于仅检查cancellationToken.IsCancellationRequested。若IBackgroundJob抛出的TaskCancelledException未触发外层cancellationToken的取消信号,内层循环可能因其他异常退出,但此时leaseCancellation的取消是否及时生效,仍取决于KeepRenewingLock对令牌的响应速度。

3. 取消令牌的传播存在潜在漏洞

RunBackgroundJobSafely中捕获异常的条件为when (!_cts.IsCancellationRequested),若IBackgroundJob抛出的TaskCancelledException并非来自_cts的取消(如Http超时),异常会被捕获并仅记录日志,不会触发leaseCancellation的取消。此时内层循环可能因Task.Delay抛出异常而退出,但finally块的leaseCancellation?.Cancel()是否能立即终止KeepRenewingLock,仍受限于该方法的取消响应能力。

修复方案

1. 改造KeepRenewingLock为异步方法,确保及时响应取消

将同步void方法改为async Task,使用支持取消的Task.Delay,并在关键节点检查取消令牌:

private async Task KeepRenewingLock(CancellationToken cancellationToken)
{
    try
    {
        while (!cancellationToken.IsCancellationRequested)
        {
            // 使用支持取消的延迟
            await Task.Delay(TimeSpan.FromSeconds(30), cancellationToken);
            
            // 检查取消令牌,避免执行不必要的续约
            cancellationToken.ThrowIfCancellationRequested();
            
            // 执行锁续约逻辑
            // code to renew lease
        }
    }
    catch (OperationCanceledException)
    {
        // 取消异常无需日志,属于预期流程
    }
    catch (Exception ex)
    {
        // code to log error
    }
}

2. 简化内层循环逻辑,明确退出条件

移除冗余的!maintainLockTask.IsCompleted判断,确保循环仅受cancellationToken控制:

while (!cancellationToken.IsCancellationRequested)
{
    await RunBackgroundJobSafely(cancellationToken);
    await Task.Delay(TimeSpan.FromMinutes(2), cancellationToken);
}

3. 优化异常处理,确保取消信号正确传递

在RunBackgroundJobSafely中,若IBackgroundJob因超时抛出TaskCancelledException,可根据业务需求决定是否触发leaseCancellation的取消,例如:

private async Task RunBackgroundJobSafely(CancellationToken cancellationToken)
{
    try
    {
        await _backgroundJob.ExecuteAsync(cancellationToken);
    }
    catch (TaskCancelledException ex) when (!_cts.IsCancellationRequested)
    {
        // 记录超时日志,并触发锁续约的取消
        // log timeout error
        throw; // 抛出异常,让外层try/catch捕获并进入finally块执行取消
    }
    catch (Exception ex) when (!_cts.IsCancellationRequested)
    {
        // log other errors
    }
}

4. 确保leaseCancellation的安全使用

在try块中初始化leaseCancellation后,可添加空值检查,避免极端场景下的空引用:

try
{
    leaseCancellation = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
    if (leaseCancellation == null)
    {
        // 日志记录并跳过后续逻辑
        return;
    }
    var maintainLockTask = Task.Run(() => KeepRenewingLock(leaseCancellation.Token), leaseCancellation.Token);
    
    // 内层循环逻辑...
}

内容的提问来源于stack exchange,提问作者Tim Butterfield

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.03 00:30:39