对象池 IObjectPool -- ESBasic 可复用的.NET类库(15)

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接口的定义如下:

    public interface IObjectPool<TObject> where TObject : class
    {
        /// <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开源前言

 

时间: 2024-10-28 13:06:58

对象池 IObjectPool -- ESBasic 可复用的.NET类库(15)的相关文章

对象获取器IObjectRetriever -- ESBasic 可复用的.NET类库(17)

1.缘起: ESBasic中许多管理对象的容器都用到了这个ESBasic.ObjectManagement.IObjectRetriever接口,所以单独将其提出来介绍一下. 当我们向对象容器(Container)请求某个对象时,也许目标对象还未加载到容器中,这可能是因为容器在初始化的时候就没有加载这个对象,也有可能是因为这个对象是容器初始化以后新增到数据库(当然也有可能是其它的持久化存储)的.在这种情况下,对象容器就可以借助IObjectRetriever来将目标对象从数据库等持久化存储中加载

ESBasic 可复用的.NET类库(00) -- 开源前言(附下载)

自从03年正式使用.NET开发以来,已经走过了6个年头,这期间我积累了几套类库和框架,ESBasic便是其中最基础的一个类库.ESBasic是Enterprise Service Basic的缩写,虽然也简写为ESB,但是它和Enterprise Service Bus(企业服务总线)没有任何关系.ESBasic是我能够快速和高效开发应用程序的利器之一,开这个专门的blog是想将它介绍给大家,希望能对大家有所启发. ESBasic覆盖的内容包括:对象管理.插件.网络(Socket).多线程.Em

优先级管理器 IPriorityManager -- ESBasic 可复用的.NET类库(14)

1.缘起:     假设我们的订单处理系统所要处理的订单是有优先级的,也就是说,不同的订单类型所要求被处理的紧迫程度不同,对那些优先级高的注单要先处理,对于优先级低的注单可稍后处理.对于处于同一优先级的订单了,就按照其到达的先后顺序进行处理.     这是一个典型的管理具有优先级的对象的需求,注单就是具有优先级(With Priority)的对象.我设计了ESBasic.ObjectManagement.Managers.IPriorityManager优先级管理器(确切地说,应该称之为"具有优

Round缓存管理器RoundCacheManager--ESBasic 可复用的.NET类库(26)

1.缘起:     在增量自动获取器章节的缘起部分,我们曾提到增量缓存,本节我们将深入探讨它以及用于管理增量缓存的管理器.我们还是以增量自动获取器章节提到的例子作为基础,并做更进一步的讨论.       OK,现在让我们开始这有趣的旅程. 首先,基于前面例子给出的上下文,我们知道IIncreaseAutoRetriever获取的增量是用于累积当天的已成交订单报表的."当天已成交报表"就是一个典型的增量缓存,每当有新的增量到来,都会累加到上面. 我们假设今天是2009.07.08,那么我

循环引擎 ICycleEngine --ESBasic 可复用的.NET类库(04)

 1.缘起: 有些系统需要每隔一段时间就执行一下某个动作,比如,一个监控系统每隔10秒钟就要检测一下被监控对象的状态是否正常,那这时我们就可以用到循环引擎了.     有人说可以使用.NET框架自带定时器如System.Threading.Timer,嗯,没错.但是若这个类使用不当可能会引发后台池线程耗尽的后果.因为Timer的定时事件触发实在后台线程池中的某个线程中处理的.也就是说Timer的每次定时事件触发都会用到一个线程,如果定时的时间间隔小于事件处理的时间,则后台线程池中将会有越来越多的

循环任务切换器 CircleTaskSwitcher -- ESBasic 可复用的.NET类库(06)

 1.缘起:     假设我的订单处理系统有这样的需求:将一天24小时分为4个时段,凌晨2:15到8:30采用A类型的处理器处理接收到的订单,8:30到14:00采用B类型的处理器,14:00到20:00采用C类型的处理器,20:00到第二天凌晨2:15采用D类型的处理器.     即我们的订单处理器需要在任一天的2:15.8:30.14:00.20:00这四个时刻发生切换,这就是一个循环切换器所要做的工作.     我设计了ESBasic.Threading.Application. ICir

工作者引擎 IWorkerEngine -- ESBasic 可复用的.NET类库(05)

1.缘起:     假设我们的系统在运行的过程中,源源不断的有新的任务需要处理(比如订单处理),而且这些任务的处理是相互独立的,没有前后顺序依赖性(顺序依赖性是指,必须在任务A处理结束后才可开始B任务),那么我们就可以使用多个线程来同时处理多个任务.每个处理任务的线程称为"工作者(线程)".      我设计了ESBasic.Threading.Engines.IWorkerEngine工作者引擎,其目的就是使用多个线程来并行处理任务,提高系统的吞吐能力.       工作者引擎的形象

回调定时器ICallbackTimer --ESBasic 可复用的.NET类库(07)

 1.缘起:     举个例子也许就能够说清楚回调定时器的用途.假设我的订单系统接收各种不同类型的订单,当订单A进来时,系统根据订单的类型和其它特征进行综合判断后,决定A订单要在2秒之后被方法M1处理:接下来收到的B订单经过同样的判断后,决定要在10秒后被方法M2处理,--.这时候就可以用回调定时器来管理这些将要被延迟一定时间再执行的任务.     当然,我们可以使用定时器或前面介绍的循环引擎来实现这样的功能,只不过我们自己需要手动管理注册的定时回调任务,并且定时检查每一个未处理订单是否已经到了

双向映射 IBidirectionalMapping -- ESBasic 可复用的.NET类库(11)

1.缘起:     假设我们的用户管理系统要求用户的ID和Name都必须是唯一的,并且用户的ID和Name一经确定就不能被修改.而且管理系统经常需要根据ID来查找Name,也经常需要根据Name来查找ID.根据这样的需求,我们可以考虑使用一个Dictionary来将ID和Name缓存起来,通常ID作为Key,Name作为Value.这样便可实现通过ID查询Name的快速查找,但是,通过Name查找ID就不是那么快了,因为涉及到对Dictionary的Values做遍历的操作.那么,有可能使得通过