【C#】从 Queue 到 ConcurrentQueue:一次对象池改造的实战心得
背景
最近在做一个图像处理的 WPF 项目,底层使用 Halcon 的 HObject
来存放图像。为了减少频繁创建和释放对象带来的开销,我实现了一个对象池,用来存放 HObject
,方便后续流程复用。
最初的实现用的是 .NET 自带的 Queue<T>
:
private readonly Queue<T> objects = new Queue<T>();
配合 lock
实现线程安全,在 GetObject
时取出一个对象,ReturnObject
时放回队列。
一切看似顺利,但随着功能扩展,我遇到了两个问题:
-
多线程调用时有潜在的性能瓶颈
lock
保护了队列,但在高并发下会阻塞其他线程。 -
对象生命周期混乱
在显示后立即归还对象时,有时会出现CurImg
已经被释放的情况,导致后续流程无法使用。
于是,我开始思考:是不是可以用 ConcurrentQueue
来替代 Queue
+ lock
?
Queue vs ConcurrentQueue 对比
特性 | Queue | ConcurrentQueue |
---|---|---|
线程安全 | ❌ 需要手动加 lock | ✅ 内置线程安全 |
性能 | 在单线程或低并发下更快 | 在多线程下更优,避免锁竞争 |
操作方式 | Enqueue / Dequeue | Enqueue / TryDequeue |
Count 属性 | 精确(单线程) | 近似值(多线程下可能不是实时) |
适用场景 | 单线程队列或少量锁保护的情况 | 高并发读写队列、生产者-消费者模式 |
改造过程
我将原先的 Queue<T>
换成了 ConcurrentQueue<T>
,并去掉了多余的 lock
。
原始版本(Queue + lock)
public T GetObject()
{lock (objects){if (objects.Count > 0)return objects.Dequeue();elsereturn new T();}
}
改造版本(ConcurrentQueue)
public T GetObject()
{if (objects.TryDequeue(out var obj)){return obj;}return new T();
}
改造后的好处
-
线程安全更自然
ConcurrentQueue
内部使用了无锁算法,减少了阻塞等待的情况。 -
代码更简洁
不再需要手动加lock
,也避免了忘记加锁导致的潜在 bug。 -
性能在多线程下更优
多个线程可以同时安全地读写队列。
需要注意的坑
改造后,我也踩了几个坑:
-
不要先判断 Count 再操作
在多线程下,Count
只是一个快照值。
如果写成:if (objects.Count > 0)objects.TryDequeue(out var obj);
就可能在判断到取出之间,队列已经被别的线程清空,导致逻辑不一致。
✅ 正确写法:直接用
TryDequeue
判断并取值。 -
Count 在容量控制上的延迟
当多个线程同时ReturnObject
,可能短暂超过_maxPoolSize
。
我的处理方式是用while
循环清理多余对象:while (objects.Count > _maxPoolSize && objects.TryDequeue(out var old)) {if (old is IDisposable disposable)disposable.Dispose(); }
虽然会有一点“超限再回落”,但影响不大。
关于 HObject 的思考
在改造过程中,我发现 HObject
这种一次性资源(Dispose
后不可复用)其实不太适合放到传统意义的“对象池”里。
但是我为了自动化释放管理,同时不愿意立马释放当前图像,所以这么做了。
总结
从这次改造中,我有几点心得:
- 如果是高并发的队列操作,
ConcurrentQueue
是更优解,省去了手动加锁的麻烦。 - 多线程下不要依赖
Count
做逻辑判断,直接用TryDequeue
更安全。 - 改造代码时,不要只关注语法,还要考虑资源生命周期,否则会出现“对象提前被释放”的问题。
💡 一句话总结:
在多线程队列管理上,
ConcurrentQueue
是比Queue+lock
更简洁的选择,但资源的生命周期管理才是对象池真正的难点。