对象分配也有乐观和悲观
最近做公司的网游项目,使用JAVA开发,网络通讯这块使用的则是mina框架。说实话,到现在也只是知道怎么用,内部实现还没仔细研究过。跟成熟的框架比起来,感觉自己之前在.NET上写的Socket在结构化程度上稍差了点,作为一名C# er,便有了模仿其API完善.NET类库的想法。这其中涉及很多技术细节,如线程同步、异步通讯、TCP协议解析、对象池使用及管理等。今天要讲的主题便是这个对象池了。
之所以要用到对象池,是因为在一个典型的TCP/IP应用中,在服务器运行期间,会有无数次收到消息,格式化成指定对象(这里暂时叫Packet吧),并且服务器处理后组织特定的Packet返回给客户端。这样如果每次都用new操作来创建Packet对象的话,加上对象的GC处理回收时间,会对性能产生一定影响。如果服务器压力不大的话,确实可以不用对象池,这样可以保持代码的简洁。不过我的看法是,即使暂时你的服务器压力瓶颈不在这,我们也可以先在底层提供这样的功能,作为技术积累,搞程序还是有点适当的前瞻意识比较好。
对象池,最简单的功能当然就是实现对象复用,避免反复创建及销毁对象。查阅了一些文章,个人总结主要的区别集中在管理策略及管理途径上。策略这东西,可以很简单,可以很复杂,因情况而异。比如.NET中的线程池中关于线程的管理策略,可细分为繁忙时的创建策略及空闲时的销毁。我们的目标是实现一个尽量通用的对象池,因此,用到的管理策略应该是尽量的简单。
再说说管理途径,一般有两种方式:
1、通过封装对象并附加一些池管理需要参数,如是否空闲,最后一次使用时间等,可以称之为ObjectWrapper;
2、通过让对象实现特定接口来实现复用,如实现IDisposable接口或自己另外定义接口。
以上两种方法各有利弊,第一种可以额外附加不同种类的参数,达到辅助管理的目的,第二种可能功能稍微单一,但是贵在简洁,这里我选的是后者,并让池化对象实现一个IPoolable接口,同时定义对象池对象接口IObjectPool,代码如下
{
/// <summary>
/// 将对象释放回对象池,务必确保事件等关联资源先清除
/// </summary>
void Free();
/// <summary>
/// 管理自身的对象池的引用
/// </summary>
IObjectPool<IPoolable> Pool { get; set; }
}
public interface IObjectPool<T> where T : IPoolable
{
/// <summary>
/// 获取空闲对象
/// </summary>
T GetFreeObject();
/// <summary>
/// 释放对象回对象池
/// </summary>
void ReleaseObject(T item);
}
Free方法要求池化对象的实现者将自身返回给对象池的空闲列表,这样做的好处在于对象的使用者可以不用关注对象池的细节。
对象的管理这一问题,确实伤了不少脑筋。使用动态管理,我认为太过复杂,如果这么做,至少要做到可销毁对象的定义,怎么样才算可销毁,是对象空闲了还是一定时间没用了,还是当前数量超出了某个值等。上层应用使用不同对象的情况各有差异,如果简单的用一个标准定死,很难通用。
另一种比较常见的方式,我称之为“乐观获取”,这种做法在无可用对象,并且对象总数达到数量上限时,会假定所有对象的使用者会及时的返回对象,此时请求者选择Sleep或者WaitOne等方式等待。我认为这是种雪上加霜的解决办法,当所有可用对象已经被使用,并且达到了数量上限,这时候认为系达到对象使用的卖方市场,即供不应求。即使有限的对象被返回,也不能满足更多的需求,最坏的情况是一些请求者一直等待。
基于此,我选择了“悲观获取”的方式,这种做法在请求对象时,保证返回一个正确对象,虽然这有可能造成系统中对象总数暂时超出了数量限制,至少可以保证不会因为没可用对象而使上层逻辑受阻。而当系统压力减轻时,可通过控制返回对象的逻辑来销毁多余的对象。
对象池代码如下:
/// 无需等待的对象池
/// </summary>
class UnWaitObjectPool<T> : IObjectPool<T> where T : IPoolable, new()
{
private int _minObjCount;
private int _maxObjCount;
private object _mutex;
private Queue<T> _freeQueue;//空闲队列
private UnWaitObjectPool(int min, int max)
{
if (min > max || min < 0 || max < 0)
{
throw new ArgumentException("illegal args.");
}
this._minObjCount = min;
this._maxObjCount = max;
this._mutex = new object();
this._freeQueue = new Queue<T>();
for (int i = 0; i < min; i++)
{
this._freeQueue.Enqueue(new T());
}
}
/// <summary>
/// 有空闲对象则直接取出,如果没有,直接创建,这里无视数量上限
/// </summary>
/// <returns></returns>
public T GetFreeObject()
{
if (this._freeQueue.Count > 0)
{
lock (this._mutex)
{
if (this._freeQueue.Count > 0)
{
return this._freeQueue.Dequeue();
}
}
}
return new T();
}
/// <summary>
/// 返回对象,每次调用后如果空闲对象超出限制,则销毁
/// </summary>
/// <param name="item"></param>
public void ReleaseObject(T item)
{
lock (this._mutex)
{
this._freeQueue.Enqueue(item);
}
this.TryTrimToMax();
}
private void TryTrimToMax()
{
while (this._freeQueue.Count > this._maxObjCount)
{
this._freeQueue.Dequeue();
}
}
}
是不是简单很多?这里少了不必要的WaitOne等线程同步操作,以及复杂的管理策略。通俗的说,这里也许跟传统概念的对象池不太一样,因为没有严格控制对象的创建,因此算是一种折中的方案,当对象需求紧张时,对象池与普通new方法一起发挥作用,两种机制同时服务于上层应用。而当系统压力一般时,对象池独自承担对象的维护。
这里只给出部分代码,忽略了单例的部分,对象池对象本身是单例的!但是我们的目的是作为简单或者默认的对象池,前文提到的Packet只是一种应用场景,既然设计成泛型,肯定是希望尽量的通用,因此如果有遗漏的地方没想到,欢迎大家批评及拍砖。
参考文章:
Java对象池技术的原理及其实现http://www.cnblogs.com/aurawing/articles/1887029.html
使用.net技术创建一个小型对象池http://www.cnblogs.com/chegan/archive/2004/03/04/2129.html
SocketAsyncEventArgs对象池示例http://msdn.microsoft.com/en-us/library/bb551675.aspx
数组缓存池示例http://msdn.microsoft.com/en-us/library/bb517542.aspx