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

Channels搭配带超时的CancellationTokenSource释放后出现内存泄漏

完整可复现代码已开源,运行可执行文件后内存会快速飙升,代码主要位于AsyncBlockingQueue.cs类中。

以下代码实现了一个简单的异步「阻塞」队列:

public async Task<T> DequeueAsync(
    int timeoutInMs = -1,
    CancellationToken cancellationToken = default)
{
    try
    {
        using (CancellationTokenSource cts = this.GetCancellationTokenSource(timeoutInMs, cancellationToken))
        {
            T value = await this._channel.Reader.ReadAsync(cts?.Token ?? cancellationToken).ConfigureAwait(false);
            return value;
        }
    }
    catch (ChannelClosedException cce)
    {
        await Console.Error.WriteLineAsync("Channel is closed.");
        throw new ObjectDisposedException("Queue is disposed");
    }
    catch (OperationCanceledException)
    {
        throw;
    }
    catch (Exception ex)
    {
        await Console.Error.WriteLineAsync("Dequeue failed.");
        throw;
    }
}


private CancellationTokenSource GetCancellationTokenSource(
    int timeoutInMs,
    CancellationToken cancellationToken)
{
    if (timeoutInMs <= 0)
    {
        return null;
    }

    CancellationTokenSource cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
    cts.CancelAfter(TimeSpan.FromMilliseconds(timeoutInMs));
    return cts;
}

按如下方式调用时会出现内存泄漏:

try
{
   string message = await this._inputQueue.DequeueAsync(10,cancellationToken).ConfigureAwait(false);
}
catch(OperationCanceledException){
   // 超时
}

内存变化截图:
内存变化截图

泄漏根因

问题核心出在CreateLinkedTokenSource生成的链接令牌源的引用持有逻辑:

  • 调用CreateLinkedTokenSource生成的令牌源,会被传入的父CancellationToken的回调列表持有引用。如果调用方传入的是生命周期和应用一致的长驻令牌(比如全局停止运行令牌),即使你用using主动释放了CTS,只要父令牌从未触发取消,这个引用关系会一直存在,导致CTS实例无法被GC回收。
  • 当前场景下设置了10ms的短超时,如果队列长期为空,每秒会生成上百个CTS实例,大量未被回收的实例和对应的取消回调不断累积,就会出现内存持续飙升的情况。

修复方案

方案一:拆分等待+读取逻辑,减少无效CTS创建(兼容所有支持Channel的.NET版本)

public async Task<T> DequeueAsync(
    int timeoutInMs = -1,
    CancellationToken cancellationToken = default)
{
    try
    {
        // 无超时场景直接读取
        if (timeoutInMs <= 0)
        {
            return await _channel.Reader.ReadAsync(cancellationToken).ConfigureAwait(false);
        }

        // 分两步:先等可读,再直接读取,避免ReadAsync长期持有CTS引用
        using var timeoutCts = new CancellationTokenSource(timeoutInMs);
        using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token);
        
        try
        {
            // 等待队列可读/超时/父令牌取消
            if (!await _channel.Reader.WaitToReadAsync(linkedCts.Token).ConfigureAwait(false))
            {
                throw new ChannelClosedException();
            }
            // 可读后直接同步读取,不需要再传令牌,避免额外引用
            return _channel.Reader.TryRead(out var value) ? value : throw new InvalidOperationException("队列读取失败");
        }
        catch (OperationCanceledException) when (timeoutCts.IsCancellationRequested)
        {
            throw new OperationCanceledException("队列读取超时");
        }
    }
    catch (ChannelClosedException)
    {
        await Console.Error.WriteLineAsync("通道已关闭");
        throw new ObjectDisposedException("队列已释放");
    }
    catch (Exception ex) when (ex is not OperationCanceledException)
    {
        await Console.Error.WriteLineAsync("出队操作失败");
        throw;
    }
}

方案二:.NET 6+ 优化方案

如果运行环境为.NET 6及以上版本,可以直接使用CancellationTokenSource的池化实现,或者直接用内置的超时判断逻辑,避免每次创建新的CTS实例,大幅降低GC压力。

如果你的业务场景超时调用非常频繁,也可以自行实现CTS复用逻辑,进一步减少对象创建开销。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.19 23:36:03