进击的对象池

来源:互联网 发布:java选择文件保存路径 编辑:程序博客网 时间:2024/06/02 22:29

前面有一篇文章说过在代码中如果要高频地创建一个对象(比如一些及时通信的socket等等)怎么通过raii技法进行内存优化,而且这些对象在程序中的生存周期比较短,搞几下就没用了,尤其是一些线程之类,如果要完成什么阻塞任务,虽然它就搞那么几秒,但放在主线程就是会卡住界面,让用户体验很差,然而线程的开销可是众所周知的不小,这个时候我们就会想到如果有一种方法能够反复利用这些对象就好了,于是就有了池这个概念,最常见的就是线程池,这个我已经用C++在win上实现了一个,写的时间比较早,可能会有一些小的问题,代码放在我的github上

https://github.com/linuxb/ThreadPoolOnWin

大家可以进行一起来完善这个线程池,以后还有在linux上写一个posix的版本,不过在本文我主要讲述对象池。

对象池的思想其实也是很简单的,不外乎就是一个可以对内存区进行控制的数据结构,我这里就使用一个链表简单粗暴,先来看看代码吧,我的代码写的还是比较易懂的:

struct LinkNode    {        /*内存块指针*/        void* _memory;        size_t _countInNode;        //每一个节点(内存块)含有的对象数量        size_t _capacity;        /*next*/        LinkNode* _nextNode;        //构造函数        LinkNode(size_t capacity)        {            if (capacity < 1)            {                throw std::invalid_argument("capacity must be at least 1");            }            if (capacity > MAX_MEMORYBLOCK)            {                throw std::invalid_argument("capacity can not out of the limit memory!");                exit(1);            }            /*Allocate*/            _memory = TAllocator::Allocate(capacity * (sizeof(U)));            if (_memory == NULL) throw std::bad_alloc();            /*init*/            _capacity = capacity;            _countInNode = 0;        }        //析构        ~LinkNode()        {            TAllocator::deAllocate(_memory, _capacity * (sizeof(U)));        }    };

看这段代码只要看看我的结构体大概有什么结构就可以了,全部的代码包括在前面RAII的代码都放在同一个github仓库里面,可以进行同时测试

https://github.com/linuxb/RaiiObjectPool

这个结构体就是对象池核心结构链表的一个节点,其中有一个指向一块内存块的指针,以及节点可容纳的对象容量,以及当前节点含有的对象数,正如你所看到,我设计的对象池会将对象创建在我指定的内存区块的指定偏移位置上,每一个节点都指向一块内存块,这块内存可以容纳一定数量的对象,具体的数量可以由我们在初始化对象池的时候认为设定,在这个内存块上,对象是连续存储的,这种整齐的内存结构也有助于我的内存管理,当对象的数量超过当前节点的最大容量时,我就会产生新的节点,这个新节点同样指向一块新的内存区块,不同节点指向的内存块是不连续的,这样使得内存的利用隔年充分(内存碎片),而且在程序中还设定了自增系数,即每一次产生新节点都会在原来的容量基础上扩容,即乘上自增系数,当然每一个节点都会有一个最大的容量上限。

了解内存的安排,再来看看如何从对象池中产生一个新的对象,为了可以实现回收,我当然还设计了一个容器来保存所有空闲内存单元(一个对象类的大小)指针,当要求产生新的对象时,先检查在空闲链表中有没有已经回收的内存,如果有,马上从链表中拿出来一个就可以使用了,这个的时间复杂度明显为O(1),还是不错的,就算运气不好,所有的对象都还在被外界指针引用,是在没法给再利用了,怎么办呢,无所谓,大不了我们再分配一块内存咯,而且这次的new是有目标的,在指定的地址进行分配,保证节点内存区块的连续,这个的复杂度也为O(1),可以先看看我实现这一个过程的代码;

template<class U,class TAllocator = MyMemoryAllocator>U* ObjectPool<U, TAllocator>::zygoteObjFromRecycle(){    if (!recycledMemoryList.empty())    {        /*memory controlled by operator*/        U* result = (U*)recycledMemoryList.front();        recycledMemoryList.pop_front();        /*initialization*/        new(result) U(0);        return result;    }    /*need to allocate new memory in the current node*/    if ((Qtail->_countInNode) >= (Qtail->_capacity)) NewNodeMemory(true);    char* addr = (char*)Qtail->_memory;    addr += (Qtail->_countInNode)*(sizeof(U));    Qtail->_countInNode++;    U* result = new(addr) U(0);    return result;}

代码很容易看懂,其实基本就是我上面阐述的意思啦,为了更加清楚地说明这个原理,我再来一张结构图吧:

这里写图片描述

如图红色部分的单元为已经被使用的内存,绿色部分为已经回收的内存块,可见,它们由空闲链表管理,外界的指针或者引用就可以通过空闲链表再次利用了。

为分配空间的性能,这里参考了stl的设计方式,采用自定义的空间配置器,在析构函数中也进行了对象的全部的销毁,避免内存泄露:

template<class U, class TAllocator = MyMemoryAllocator>void ObjectPool<U, TAllocator>::destroyObjPool(){    //销毁链表    LinkNode* p = QHeader;    LinkNode* q = p;    if (p == NULL) return;    while (p != NULL)    {        p = p->_nextNode;        delete q;        q = p;    }}

空闲链表由于在程序中作为全局变量的成员变量,cpu会在对象池析构时收拾它,其实在全部的内存都释放之后,里面的指针已经完全失效了,家都被炸毁了,就算你认识回家的路,你还能回得去么,在这里再次delete会引发程序的迷失,pc都不知道跑什么地方去,有兴趣的可以那我的代码尝试一下,我的测试是没有发现内存泄露问题的,而且创建几百甚至上千个对象反复折腾所耗费的时间也是毫秒级别的,所以说对象池模式在高频变量创建中是可以提高性能以及节约内存空间,比如我的测试类有4bytes,我创建1000个,在删除再创建1000个,很明显对象池节约了差不多4KB的内存,可以完成内存优化,接下来会有关于linux下线程池的文章,本次到此结束咯。

1 0
原创粉丝点击