【C#】用队列构建一个对象池管理对象的创建和释放

发布于:2025-08-14 ⋅ 阅读:(21) ⋅ 点赞:(0)

背景

最近在做一个图像处理的 WPF 项目,底层使用 Halcon 的 HObject 来存放图像。为了减少频繁创建和释放对象带来的开销,我实现了一个对象池,用来存放 HObject,方便后续流程复用。

最初的实现用的是 .NET 自带的 Queue<T>

private readonly Queue<T> objects = new Queue<T>();

配合 lock 实现线程安全,在 GetObject 时取出一个对象,ReturnObject 时放回队列。

由于代码会有并发情况,所以直接将Queue 改成了 ConcurrentQueue,就可以不用使用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();
        else
            return new T();
    }
}

改造版本(ConcurrentQueue)

public T GetObject()
{
    if (objects.TryDequeue(out var obj))
    {
        return obj;
    }
    return new T();
}

改造后的好处

  1. 线程安全更自然
    ConcurrentQueue 内部使用了无锁算法,减少了阻塞等待的情况。

  2. 代码更简洁
    不再需要手动加 lock,也避免了忘记加锁导致的潜在 bug。

  3. 性能在多线程下更优
    多个线程可以同时安全地读写队列。


需要注意的坑

改造后,我也踩了几个坑:

  1. 不要先判断 Count 再操作
    在多线程下,Count 只是一个快照值。
    如果写成:

    if (objects.Count > 0)
        objects.TryDequeue(out var obj);
    

    就可能在判断到取出之间,队列已经被别的线程清空,导致逻辑不一致。

    正确写法:直接用 TryDequeue 判断并取值。

  2. 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 更简洁的选择,但资源的生命周期管理才是对象池真正的难点。


网站公告

今日签到

点亮在社区的每一天
去签到