1.缘起:
对象池应该是一个“历史悠久”的概念了,像我们经常说的线程池、还有ADO.NET中的数据库连接池等,都属于对象池的应用。
我们的应用有时也会碰到需要使用对象池的情况,我举个例子说明一下。假设,我们需要记录某个类MyClass的每个方法每次被调用时方法执行所消耗的时间,而且,这个类是使用在多线程的环境中的,每个方法都可以同时在多个线程中执行,不需要被同步,这样可以使并发达到最大。
好,我们可以使用Stopwatch这个类来准确地记录每个方法的时间,关键是怎么使用它?为MyClass定义一个Stopwatch类型的成员变量,然后在每个方法的开始调用这个成员的Start以启动计时,在方法返回之前记录耗费的时间,然后Reset这个Stopwatch成员。
这种方案只有在一种情况下可以良好工作,那就是要求对MyClass的所有方法的调用都是同步的,而且多个线程调用同一个方法时也必须被同步,否则,Stopwatch的计时就会错乱了。
那么如何解决了?实际上很简单,我们不需要为MyClass定义一个Stopwatch类型的成员变量,而是在每个方法的入口处,new一个Stopwatch类型的局部变量,这个局部变量只服务于当前方法的一次调用。也就是说,对MyClass的任何一个方法的任何一次调用,都会产生一个Stopwatch类型的对象专门用于这次调用的计时工作,当这次调用返回后,该Stopwatch对象就可以被销毁了。这样就可以达到我们最初假设的需求。
从解决方案我们看到,每次调用都会新建一个Stopwatch对象,当调用返回时,这个对象就没有存在的价值而可以被销毁了。这样的结果就是会反复地创建并销毁Stopwatch类型的对象。
这正是使用对象池的一个绝佳场合,我们把一些可用的Stopwatch对象放进对象池中,每次方法被调用时,就向对象池租借一个Stopwatch对象来进行计时,调用返回时,再将这个对象归还给对象池即可。这样就避免了Stopwatch对象的重复创建和销毁。
我设计的ESBasic.ObjectManagement.Pool.IObjectPool就是一个通用的对象池,它是泛型的,所以可以池化存储不同类型的对象。
2.适用场合:
根据我们上面的描述,我们可以总结出当有类似以下的需求时,可以使用对象池技术。
(1)某个类型的对象经常被重复的创建、销毁。
(2)每个该类型的对象被使用的时间都很短。
(3)使用一个共享的对象无法达到系统的要求(比如会限制最大并发量)。
(4)相对于新建或销毁一个对象来说,清除对象的状态要容易得多。
3.设计思想与实现
IObjectPool接口的定义如下:
{
/// <summary>
/// MinObjectCount 对象池中最少同时存在的对象数。
/// </summary>
int MinObjectCount { get ; set ; }
/// <summary>
/// MaxObjectCount 对象池中最多同时存在的对象数。
/// </summary>
int MaxObjectCount { get ; set ; }
/// <summary>
/// DetectSpanInMSecs 当池中没有空闲的对象且数量已达到MaxObjectCount时,如果这时发生Rent调用,则检测空闲对象的时间间隔。
/// 默认值为10ms。
/// </summary>
int DetectSpanInMSecs { get ; set ; }
/// <summary>
/// PooledObjectCreator 用于创建池中对象的创建器。默认为DefaultPooledObjectCreator
/// </summary>
IPooledObjectCreator < TObject > PooledObjectCreator { set ; }
void Initialize();
TObject Rent();
void GiveBack(TObject obj);
}
这个接口有一个泛型参数:TObject,表示我们要池化的对象的类型。泛型约束表明能够放入对象池中的对象必须是引用类型。
MinObjectCount属性指示对象池在初始化的时候就必须确保池中存在的对象的数量。
MaxObjectCount属性表示对象池最多能够容纳的对象数量。
如果一个对象被租借出去,则对象池会将其状态标记为“繁忙”的;如果一个对象没有被租借出去或被归还,则其状况就是“空闲”的。
对于ObjectPool的实现,要注意以下几点:
(1)对象池是多线程安全的,可以在多线程的环境下使用。我们对内部的集合进行了加锁控制。
(2)对象池并不直接负责对象的创建工作,它把这项职责委托给了池化对象创建者IPooledObjectCreator。
(3)池化对象创建者IPooledObjectCreator不仅负责对象的创建工作,而且也负责清除对象的状态(Reset方法)。在GiveBack方法的内部就有调用Reset方法来清除对象的遗留状态的。IPooledObjectCreator接口的定义如下所示:
/// <summary>
/// IPooledObjectCreator 池化对象创建者。用于创建被池缓存的对象。并能清除对象的状态。
/// </summary>
public interface IPooledObjectCreator<TObject> where TObject : class
{
TObject Create();
void Reset(TObject obj);
}
4. 使用时的注意事项
(1)当外部调用Rent方法向对象池租借一个对象时,如果对象池中没有“空闲”的对象,并且池中的对象的数量已经达到了MaxObjectCount,那么这时该如何处理了?ObjectPool采用的策略是选择等待,等待直到有对象变成“空闲”,否则就一直阻塞当前线程。你必须注意到ObjectPool采用的这个策略可能会与你的期望不一致。
(2)当对象池中的空闲对象很多时,即使已经远远地大于了MinObjectCount的值,对象池也不会释放其中的某些对象,而是一直保持着。MinObjectCount只是决定了池在初始化的时候应该创建的对象的数量以备用。
(3)基于上面的两点原因,所以我们在具体应用时需要谨慎地为MaxObjectCount设定一个合理的值。如果这个值太小,可能会使得阻塞线程的情况经常发生。当然,这个值也不是设得越大越好,因为如果平时空闲的对象很多,就表示要占用更多的资源而却没有发挥出相应的价值。
(4)一个从池中借出的对象在被归还回给池的时候,必须把上次使用时遗留的状态清除掉,否则后面的租借者可能会误用其遗留的状态。
(5)如果清除一个对象的状态很不容易,相反创建和销毁一个对象却非常容易,那么这时可以考虑不使用对象池,而是每次new一个对象来使用,使用完了就丢弃掉。
5.扩展
如果我们要池化的对象是没有状态的,而且其类型也有不带参数的默认构造函数,那么我们可以直接使用ESBasic提供的默认池化对象创建者DefaultPooledObjectCreator。DefaultPooledObjectCreator使用反射创建对象,并且Reset方法也不对对象做任何供动作――因为对象本身就是没有状态的,所以也就不存在清除其状态的需要了。
注: ESBasic已经开源,点击这里下载源码。
ESBasic开源前言