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]
实际实现中:
- 为每个源令牌注册回调函数
- 当任一源令牌取消时,触发链接令牌源的取消
- 回调中调用
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 字节
💡 提示:在频繁创建的场景中,考虑使用对象池
总结:链接令牌源使用原则
- 组合原则:当操作需要响应多个取消条件时使用
- 释放原则:始终使用
using
或显式Dispose()
- 诊断原则:在 catch 块中检查源令牌确定原因
- 性能原则:避免深层嵌套,扁平化令牌组合
- 分层原则:在复杂系统中建立分层取消架构
- 资源原则:为每个链接令牌注册资源清理回调
通过掌握链接令牌源,我们可以构建出响应灵敏、资源管理完善的高质量并发应用,特别是在处理混合并行任务和复杂取消场景时,这是不可或缺的高级技术。