在多线程编程中,生产者-消费者模式是最常见的场景之一。BlockingCollection
我第一次在实际项目中使用 BlockingCollection
BlockingCollection
csharp复制// 默认构造函数使用ConcurrentQueue
var collection = new BlockingCollection<int>();
// 也可以显式指定其他实现
var stackCollection = new BlockingCollection<int>(new ConcurrentStack<int>());
有趣的是,BlockingCollection
BlockingCollection
csharp复制// 简化版的阻塞逻辑示意
lock (syncObj) {
while (collection.Count == 0) {
Monitor.Wait(syncObj);
}
return collection.Take();
}
实际实现比这复杂得多,因为它需要考虑超时、取消令牌等多种情况,但基本原理类似。我曾在高并发场景下测试过,这种阻塞机制的性能比简单的轮询要好得多。
BlockingCollection
这种严谨的边界处理在实际项目中非常重要,特别是在需要优雅关闭的长时间运行服务中。
BlockingCollection
csharp复制// 创建有界集合
var boundedCollection = new BlockingCollection<int>(1000);
经验法则:
在我的日志处理系统中,经过多次调优发现,将容量设置为消费者1秒内能处理的最大消息量的2倍效果最佳。
生产者端的典型模式:
csharp复制// 多生产者示例
Parallel.For(0, producerCount, i => {
while (moreWorkToDo) {
var item = ProduceNextItem();
try {
collection.Add(item); // 可能阻塞
} catch (InvalidOperationException) {
// 集合已标记为完成添加
break;
}
}
});
关键点:
消费者端的推荐做法:
csharp复制// 使用GetConsumingEnumerable简化消费者代码
foreach (var item in collection.GetConsumingEnumerable()) {
ProcessItem(item);
}
// 带取消令牌的版本
foreach (var item in collection.GetConsumingEnumerable(cancellationToken)) {
ProcessItem(item);
}
GetConsumingEnumerable 是一个非常有用的方法,它会在集合完成且为空时自动终止循环。我在实际项目中发现,这种方法比手动检查 IsCompleted 更可靠。
优雅关闭是一个经常被忽视但极其重要的话题。正确的关闭流程:
csharp复制// 生产者停止生产
// ...
// 标记集合为完成
collection.CompleteAdding();
// 消费者会自然完成
await consumerTask;
BlockingCollection
csharp复制var stage1 = new BlockingCollection<InputData>();
var stage2 = new BlockingCollection<ProcessedData>();
// 第一阶段处理
Task.Run(() => {
foreach (var input in stage1.GetConsumingEnumerable()) {
var processed = ProcessStage1(input);
stage2.Add(processed);
}
stage2.CompleteAdding();
});
// 第二阶段处理
Task.Run(() => {
foreach (var data in stage2.GetConsumingEnumerable()) {
ProcessStage2(data);
}
});
这种模式在我参与的一个ETL系统中表现优异,每个处理阶段都可以独立扩展。
对于高频小项,批处理可以显著提高性能:
csharp复制// 批量消费者实现
var buffer = new List<T>(batchSize);
foreach (var item in collection.GetConsumingEnumerable()) {
buffer.Add(item);
if (buffer.Count >= batchSize) {
ProcessBatch(buffer);
buffer.Clear();
}
}
if (buffer.Count > 0) {
ProcessBatch(buffer);
}
在一个物联网项目中,这种批处理方式将数据库写入性能提升了8倍。
BlockingCollection
我曾经调试过一个内存泄漏问题,最终发现是因为忘记调用 CompleteAdding,导致消费者线程永远不会退出。
建议添加简单的监控:
csharp复制// 简单的集合监控
Timer timer = new Timer(_ => {
Console.WriteLine($"Queue depth: {collection.Count}");
}, null, 1000, 1000);
对于生产系统,更完善的指标收集是必要的,比如:
.NET Core 引入了 Channel
| 特性 | BlockingCollection |
Channel |
|---|---|---|
| 内存效率 | 中等 | 高 |
| 吞吐量 | 中等 | 高 |
| API 复杂度 | 低 | 中等 |
| 同步/异步支持 | 仅同步 | 两者 |
| 背压支持 | 有界集合 | 内置 |
对于新项目,特别是异步代码,Channel
在某些极端性能要求的场景下,可能需要考虑自定义实现。但根据我的经验,BlockingCollection
只有在性能分析明确显示它是瓶颈时,才应该考虑自定义实现。