.NET Core中ConcurrentQueue.Count性能过慢的解决办法咨询
你遇到的这个问题确实是.NET Core 2.x版本里ConcurrentQueue的一个已知设计缺陷——这个版本的Count属性并非O(1)操作,它需要遍历整个队列的内部链表来统计元素数量,再加上额外的ETW事件初始化开销(也就是你看到的System.Collections.Concurrent.CDSCollectionETWBCLProvider.ctor()),队列越大性能损耗就越夸张,完全不适合用来做流量控制的判断依据。
下面给你几个在.NET Core 2环境下可行的解决办法:
方法1:自行维护原子计数器
既然ConcurrentQueue.Count不靠谱,我们可以用原子操作手动跟踪队列元素数量,这是最直接的替代方案:
private readonly ConcurrentQueue<YourItemType> _queue = new ConcurrentQueue<YourItemType>(); private int _currentQueueCount = 0; private const int MaxQueueSize = 20000; // 入队任务逻辑 while (reader.Read()) { // 安全读取当前计数,超过阈值则等待 while (Interlocked.CompareExchange(ref _currentQueueCount, 0, 0) >= MaxQueueSize) { Thread.Sleep(10); // 异步场景可替换为await Task.Delay(10) } // 入队并原子更新计数 var item = /* 构造你的数据项 */; _queue.Enqueue(item); Interlocked.Increment(ref _currentQueueCount); } // 出队处理任务逻辑 while (true) { if (_queue.TryDequeue(out var item)) { // 处理数据项 Interlocked.Decrement(ref _currentQueueCount); } else { // 队列为空时短暂等待 Thread.Sleep(10); } }
这里用Interlocked.CompareExchange安全读取当前计数,Increment和Decrement保证计数更新的原子性,所有操作都是O(1)级别的,性能开销可以忽略不计。
方法2:使用SemaphoreSlim做流量控制
这是更优雅、更符合并发设计最佳实践的方案——用信号量直接限制入队的许可数量,从根源上避免队列过度增长,完全不需要依赖队列的Count属性:
private readonly ConcurrentQueue<YourItemType> _queue = new ConcurrentQueue<YourItemType>(); private readonly SemaphoreSlim _queueSemaphore; private const int MaxQueueSize = 20000; // 初始化信号量,初始许可数等于最大队列容量 public YourClass() { _queueSemaphore = new SemaphoreSlim(MaxQueueSize, MaxQueueSize); } // 异步入队任务 async Task EnqueueTask() { while (reader.Read()) { // 等待获取许可,直到队列有空闲位置 await _queueSemaphore.WaitAsync(); try { var item = /* 构造你的数据项 */; _queue.Enqueue(item); } catch { // 入队失败时释放许可,避免信号量泄漏 _queueSemaphore.Release(); throw; } } } // 异步出队处理任务 async Task ProcessTask() { while (true) { if (_queue.TryDequeue(out var item)) { try { // 处理数据项 } finally { // 处理完成后释放许可,允许新的入队操作 _queueSemaphore.Release(); } } else { // 队列为空时短暂等待 await Task.Delay(10); } } }
SemaphoreSlim的WaitAsync会自动阻塞直到有可用许可,处理完成后释放许可,整个流程不需要手动计数,既安全又高效。
额外建议:升级.NET Core版本
如果你有条件升级到.NET Core 3.0及以上版本,这个问题已经被官方修复了——后续版本的ConcurrentQueue重新实现了Count属性,改为O(1)的原子计数,不会再出现遍历队列和ETW开销的问题,直接用你原来的逻辑就能正常工作。
内容的提问来源于stack exchange,提问作者Douglas Gaskell

