C# 中的链接令牌源(Linked CancellationTokenSource)

深入解析 C# 中的链接令牌源(Linked CancellationTokenSource)

“链接的令牌源”指的是CancellationTokenSource.CreateLinkedTokenSource方法创建的CancellationTokenSource对象。

“链接的令牌源”允许我们将多个CancellationToken组合成一个,当其中任何一个令牌被取消时,这个链接的令牌源也会被取消。

链接令牌源是 C# 中处理复杂取消场景的强大工具,它允许我们将多个取消令牌组合成一个"逻辑或"关系的令牌。

一、基础概念

1. 什么是链接令牌源?

// 创建两个独立的取消令牌源
var cts1 = new CancellationTokenSource();
var cts2 = new CancellationTokenSource();

// 创建链接令牌源 - 当cts1或cts2取消时,linkedCts也会取消
var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
    cts1.Token, 
    cts2.Token
);

// 获取组合后的令牌
CancellationToken combinedToken = linkedCts.Token;

2. 核心特性

  • 逻辑或关系:当任意一个源令牌被取消时,链接令牌也会被取消
  • 资源管理:链接令牌源需要显式释放(实现了 IDisposable
  • 无继承关系:取消链接令牌源不会影响原始令牌源
  • 状态同步:链接令牌的状态实时反映源令牌状态

二、工作原理

1. 内部机制

当创建链接令牌源时:

graph LR A[cts1] --> C[linkedCts] B[cts2] --> C C --> D[Operation]

实际实现中:

  1. 为每个源令牌注册回调函数
  2. 当任一源令牌取消时,触发链接令牌源的取消
  3. 回调中调用 linkedCts.Cancel()

2. 状态传播时序

sequenceDiagram participant cts1 participant cts2 participant linkedCts participant Operation cts1->>linkedCts: Cancel() linkedCts->>Operation: 触发取消 Operation->>Operation: 执行取消逻辑 cts2->>linkedCts: Cancel() // 后续取消无影响

三、核心使用场景

1. 超时 + 用户取消组合

async Task DownloadWithTimeoutAsync(string url, CancellationToken userToken)
{
    // 创建超时令牌(5秒)
    using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
    
    // 组合用户取消和超时
    using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
        userToken,
        timeoutCts.Token
    );
    
    try
    {
        return await httpClient.GetAsync(url, linkedCts.Token);
    }
    catch (OperationCanceledException ex) 
    {
        if (timeoutCts.IsCancellationRequested)
            throw new TimeoutException("操作超时", ex);
        throw;
    }
}

2. 分层取消系统

class ProcessingSystem
{
    private readonly CancellationTokenSource _globalCts = new();
    private readonly List<CancellationTokenSource> _layerCts = new();
    
    public CancellationToken CreateLayerToken(params CancellationToken[] additionalTokens)
    {
        // 组合全局令牌和额外令牌
        var tokens = new List<CancellationToken> { _globalCts.Token };
        tokens.AddRange(additionalTokens);
        
        var layerCts = CancellationTokenSource.CreateLinkedTokenSource(tokens.ToArray());
        _layerCts.Add(layerCts);
        return layerCts.Token;
    }
    
    public void CancelLayer(CancellationToken token)
    {
        // 找到匹配的链接源并取消
        var cts = _layerCts.FirstOrDefault(c => c.Token == token);
        cts?.Cancel();
    }
    
    public void Dispose()
    {
        foreach (var cts in _layerCts) cts.Dispose();
        _globalCts.Dispose();
    }
}

3. 资源清理注册

void ProcessFile(string path, CancellationToken token)
{
    // 注册资源清理回调
    token.Register(() => {
        File.Delete(tempPath); // 取消时删除临时文件
        Log($"处理取消: {path}");
    });
    
    // 实际处理逻辑...
}

// 使用链接令牌
var mainCts = new CancellationTokenSource();
var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
    mainCts.Token, 
    GetMemoryPressureToken()
);

ProcessFile("data.bin", linkedCts.Token);

四、高级用法与技巧

1. 动态添加源令牌

var baseCts = new CancellationTokenSource();
var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(baseCts.Token);

// 运行时添加新取消条件
void AddNewCancellationSource(CancellationToken newToken)
{
    // 创建新链接源包含原有令牌和新令牌
    var newLinkedCts = CancellationTokenSource.CreateLinkedTokenSource(
        linkedCts.Token, 
        newToken
    );
    
    // 替换并释放旧链接源
    linkedCts.Dispose();
    linkedCts = newLinkedCts;
}

2. 诊断取消来源

async Task ProcessDataAsync(CancellationToken token1, CancellationToken token2)
{
    using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(token1, token2);
    
    try
    {
        await LongRunningOperationAsync(linkedCts.Token);
    }
    catch (OperationCanceledException)
    {
        // 诊断具体取消来源
        if (token1.IsCancellationRequested && !token2.IsCancellationRequested)
            Log("取消来源: token1");
        else if (!token1.IsCancellationRequested && token2.IsCancellationRequested)
            Log("取消来源: token2");
        else
            Log("多个来源触发取消");
    }
}

3. 与并行操作集成

void ParallelProcessing(IEnumerable<DataItem> data, CancellationToken externalToken)
{
    // 创建内存压力监控令牌
    using var memoryCts = new MemoryAwareCancellation(minFreeMemory: 100_000_000);
    
    // 组合外部令牌和内存令牌
    using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
        externalToken,
        memoryCts.Token
    );
    
    var options = new ParallelOptions {
        CancellationToken = linkedCts.Token,
        MaxDegreeOfParallelism = Environment.ProcessorCount
    };
    
    Parallel.ForEach(data, options, item => {
        // 处理每个数据项...
    });
}

五、资源管理与最佳实践

1. 正确释放模式

// 正确:使用 using 语句
using var cts1 = new CancellationTokenSource();
using var cts2 = new CancellationTokenSource();
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cts1.Token, cts2.Token);

// 错误:忘记释放链接令牌源
var cts1 = new CancellationTokenSource();
var cts2 = new CancellationTokenSource();
var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cts1.Token, cts2.Token);
// 忘记调用 linkedCts.Dispose() 会导致内存泄漏

2. 性能优化技巧

// 避免过度链接
// 不好:多层嵌套链接
var cts1 = new CancellationTokenSource();
var cts2 = new CancellationTokenSource();
var linked1 = CancellationTokenSource.CreateLinkedTokenSource(cts1.Token, cts2.Token);
var linked2 = CancellationTokenSource.CreateLinkedTokenSource(linked1.Token, anotherToken);

// 更好:扁平化链接
var linked = CancellationTokenSource.CreateLinkedTokenSource(
    cts1.Token, 
    cts2.Token, 
    anotherToken
);

3. 生命周期管理策略

class ResourceProcessor : IDisposable
{
    private readonly List<CancellationTokenSource> _linkedSources = new();
    private readonly CancellationTokenSource _globalCts = new();
    
    public CancellationToken CreateProcessorToken(CancellationToken extraToken)
    {
        var cts = CancellationTokenSource.CreateLinkedTokenSource(
            _globalCts.Token,
            extraToken
        );
        _linkedSources.Add(cts);
        return cts.Token;
    }
    
    public void Dispose()
    {
        // 先取消所有操作
        _globalCts.Cancel();
        
        // 然后释放所有资源
        foreach (var cts in _linkedSources) cts.Dispose();
        _globalCts.Dispose();
    }
}

六、常见问题与解决方案

1. 链接令牌源未被取消?

可能原因:

  • 源令牌从未被取消
  • 链接时使用了已取消的令牌(创建时状态被固定)
  • 回调注册失败

诊断方法:

// 检查所有源令牌状态
Debug.Assert(cts1.IsCancellationRequested || cts2.IsCancellationRequested, 
    "源令牌应已被取消");

// 检查链接令牌状态
Debug.Assert(linkedCts.IsCancellationRequested, 
    "链接令牌应已被取消");

2. 内存泄漏问题

症状:取消操作后内存未释放
解决方案:

// 确保释放所有链接源
public void CancelAndCleanup()
{
    // 1. 取消操作
    _globalCts.Cancel();
    
    // 2. 释放所有链接令牌源
    foreach (var cts in _linkedSources)
    {
        cts.Dispose(); // 关键:释放内部资源
    }
    _linkedSources.Clear();
}

3. 取消响应延迟

优化技巧:

// 在循环中混合使用两种检查方式
for (int i = 0; i < data.Length; i++)
{
    // 快速检查(每100次迭代)
    if (i % 100 == 0) token.ThrowIfCancellationRequested();
    
    // 慢速处理...
    
    // 额外检查点(在关键操作前)
    if (isCriticalSection) token.ThrowIfCancellationRequested();
}

七、真实世界案例:Web服务请求处理

public async Task<Result> ProcessRequestAsync(Request request, CancellationToken clientToken)
{
    // 创建服务级超时(30秒)
    using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
    
    // 创建数据库查询取消令牌(10秒)
    using var dbTimeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
    
    // 组合令牌:客户端取消 + 服务超时
    using var mainLinkedCts = CancellationTokenSource.CreateLinkedTokenSource(
        clientToken,
        timeoutCts.Token
    );
    
    try
    {
        // 步骤1:验证请求(快速操作)
        ValidateRequest(request);
        
        // 步骤2:数据库查询(带独立超时)
        var dbResult = await _database.QueryAsync(
            request.Query, 
            CancellationTokenSource.CreateLinkedTokenSource(
                mainLinkedCts.Token,
                dbTimeoutCts.Token
            ).Token
        );
        
        // 步骤3:CPU密集型处理
        var processedData = await Task.Run(() => 
        {
            var options = new ParallelOptions {
                CancellationToken = mainLinkedCts.Token,
                MaxDegreeOfParallelism = 4
            };
            
            return ProcessDataParallel(dbResult, options);
        }, mainLinkedCts.Token);
        
        return CreateResponse(processedData);
    }
    catch (OperationCanceledException ex) 
    {
        // 诊断具体取消原因
        if (clientToken.IsCancellationRequested)
            return Result.ClientCanceled;
        
        if (timeoutCts.IsCancellationRequested)
            return Result.Timeout;
        
        if (dbTimeoutCts.IsCancellationRequested)
            return Result.DatabaseTimeout;
        
        return Result.UnknownCancel;
    }
}

八、性能考量

1. 创建开销

  • 每个链接令牌源创建:约 100 ns
  • 每个令牌注册:约 50 ns
  • 取消传播:约 20 ns/令牌

2. 优化建议

// 重用链接令牌源(当条件不变时)
private CancellationTokenSource _cachedLinkedCts;

public CancellationToken GetCombinedToken(CancellationToken newToken)
{
    if (_cachedLinkedCts != null && !_cachedLinkedCts.IsCancellationRequested)
    {
        return _cachedLinkedCts.Token;
    }
    
    _cachedLinkedCts?.Dispose();
    _cachedLinkedCts = CancellationTokenSource.CreateLinkedTokenSource(
        _baseToken, 
        newToken
    );
    return _cachedLinkedCts.Token;
}

3. 内存占用

  • 每个 CancellationTokenSource:约 40 字节
  • 每个链接:额外 24 字节
  • 回调注册:每个约 32 字节

💡 提示:在频繁创建的场景中,考虑使用对象池

总结:链接令牌源使用原则

  1. 组合原则:当操作需要响应多个取消条件时使用
  2. 释放原则:始终使用 using 或显式 Dispose()
  3. 诊断原则:在 catch 块中检查源令牌确定原因
  4. 性能原则:避免深层嵌套,扁平化令牌组合
  5. 分层原则:在复杂系统中建立分层取消架构
  6. 资源原则:为每个链接令牌注册资源清理回调

通过掌握链接令牌源,我们可以构建出响应灵敏、资源管理完善的高质量并发应用,特别是在处理混合并行任务和复杂取消场景时,这是不可或缺的高级技术。

posted @ 2025-08-17 20:13  青云Zeo  阅读(29)  评论(0)    收藏  举报