【今日/总数】文章:0/2774 用户:5/10791 书籍:0/200
回顾2016,喜迎2017,以“回顾我与C++技术网的2016,展望C++技术网的2017”为主题,发表文章,将有机会赢得2个月的会员或现金红包。

当前位置:工作生活 -> 提问 -> [C++] 内存池架构设计问题

原创版权标志[C++] 内存池架构设计问题

作者:Snow  发表时间:2017/1/3 21:42:07  阅读:129
[摘要]C++技术网已经用心解答了用户提出的问题【[C++] 内存池架构设计问题】,如还有问题,请继续在文章底部留言交流,直到完全懂了为止。
文章来源:C++技术网 原创文章版权所有,未经授权,禁止转载。

内存池架构设计问题

 

         通常来讲,当程序需要大量的申请与释放堆内存操作时,都需要自定义一个内存池,否则有可能会产生野指针,产生内存碎片,申请与释放速度较慢,程序不能长时间运行。

 

         例如在Windows 的服务器编程中,就需要内存池的操作。

 

         通过C++技术网 的帮助,我学会了IO完成端口的编写。虽然是效率很高,但是到了最后发现,IO完成端口的错误处理也是最多的。例如 GetQueuedCompletionStatus、 PostQueuedCompletionStatus、GetOverlappedResult、AcceptEx、WSASend、WSARecv、DisconnectEx等函数中返回FALSE之后 根据GetLastError的返回值处理。但是错误的返回值有很多种,比如本地的网络连接被关闭、内存不足、队列已满等。感觉这个错误处理需要时间的累积,怪不得有些公司需要人员长期维护,而不是一次性的编写好。让我不得不感叹,好处越多,坏处也跟着一起多了起来。

 

         回归正题,这次我所说的是内存池架构设计的问题。

 

         首先我先把我的一个简单设计的内存池代码写出来:

 

         1.定义枚举常量:

 

         enum  NodeType

         {

                  NODE_RETREAT,            // 未被占用内存

                  NODE_OCCUPY               // 已被占用内存

         };

 

         这个枚举常量我是在后来加上去的,为了避免释放指针的时候重复释放而设置的。

 

         2.定义内存池链表节点:

 

         template <class  T>

         struct  MemNode  :  public  T

         {

                  MemNode*      m_pPrev;          // 上一个内存

                  MemNode*      m_pNext;          // 下一个内存

                  NodeType         m_Type;           // 节点类型

         };

 

         这里可能要说我为什么选择派生自定义类型,而不是选择如下定义的结构体呢?

 

         template <class  T>

         struct  MemNode

         {

                   T                         m_Data;            // 自定义类型

                  MemNode*      m_pPrev;          // 上一个内存

                  MemNode*      m_pNext;          // 下一个内存

                  NodeType         m_Type;           // 节点类型

         };

 

         如果自定义类型中也有个成员变量叫 m_pPrev 的话就会出现同名错误现象,为了解决这个问题,所以选择了派生自定义类型,成员变量名称不会造成冲突,但是我这个派生的话有个缺点,就是没有办法派生基础类型,比如charshortintlong等基础类型。

 

         如果非要用基础类型的话,只好定义一个自定义的结构体,例子如下:

 

         struct  MyStruct

         {

                   int              m_nInt;

         };

 

         这样访问速度应该和普通访问int类型差不多。

 

       3.定义内存池链表类:

 

       template<class  T>

         class  CMemList

         {

         public:

                  CMemList();

                  ~CMemList();

 

         public:

                   // 把申请的新节点插入到链表末尾处,本函数不负责申请内存,请外部申请

                  VOID        PushBackNode(MemNode<T>* pNode);

 

                   // 把某节点移除出链表,本函数不负责释放内存,请外部释放

                  VOID        EraseNode(MemNode<T>* pNode);

 

                   // 把末尾处的节点移除掉,并赋值给pNode,如果链表为空,不会进行赋值操作,本函数不负责释放内存,请外部释放

                  VOID        PopBackNode(MemNode<T>*& pNode);

 

                   // 返回当前链表的数量

                  SIZE_T    GetListCount();

 

         private:

                  MemNode<T>*        m_pHead;                  // 链表头指针

                  MemNode<T>*        m_pTail;                     // 链表尾指针

                  SIZE_T                       m_stCount;                // 记录链表数量

         };

 

         template<class T>

         inline CMemList<T>::CMemList()

         {

                  m_pHead = m_pTail = NULL;

                  m_stCount = 0;

         }

 

         template<class T>

         inline CMemList<T>::~CMemList()

         {

                  m_pHead = m_pTail = NULL;

                  m_stCount = 0;

         }

 

         template<class T>

         inline VOID CMemList<T>::PushBackNode(MemNode<T>* pNode)

         {

                  if (NULL == m_pHead && NULL == m_pTail)

                  {

                           pNode->m_pPrev = NULL;

                           pNode->m_pNext = NULL;

 

                           m_pHead = m_pTail = pNode;

                  }

                  else

                  {

                           m_pTail->m_pNext = pNode;

 

                           pNode->m_pPrev = m_pTail;

                            pNode->m_pNext = NULL;

 

                            m_pTail = pNode;

                  }

 

                  ++m_stCount;

         }

 

         template<class T>

         inline VOID CMemList<T>::EraseNode(MemNode<T>* pNode)

         {

                  if (m_pHead == pNode && m_pTail == pNode)

                  {

                           m_pHead = m_pTail = NULL;

                  }

                  else if (m_pHead == pNode)

                  {

                           m_pHead = m_pHead->m_pNext;

                           m_pHead->m_pPrev = NULL;

                  }

                  else if (m_pTail == pNode)

                  {

                           m_pTail = m_pTail->m_pPrev;

                           m_pTail->m_pNext = NULL;

                  }

                  else

                  {

                           MemNode<T>* pPrev = pNode->m_pPrev;

                            MemNode<T>* pNext = pNode->m_pNext;

 

                           pPrev->m_pNext = pNode->m_pNext;

                            pNext->m_pPrev = pPrev;

                  }

 

                  --m_stCount;

         }

 

         template<class T>

         inline VOID CMemList<T>::PopBackNode(MemNode<T>*& pNode)

         {

                  if (NULL != m_pHead && NULL != m_pTail)

                  {

                           pNode = m_pTail;

 

                           if (NULL == m_pTail->m_pPrev && NULL == m_pTail->m_pNext) {

                                     m_pHead = m_pTail = NULL;

                           }

                            else

                            {

                                     MemNode<T>* pPrev = m_pTail->m_pPrev;

                                     pPrev->m_pNext = NULL;

 

                                     m_pTail = pPrev;

                            }

 

                            --m_stCount;

                  }

         }

 

         template<class T>

         inline SIZE_T CMemList<T>::GetListCount()

         {

                  return m_stCount;

         }

 

         代码都是经过测试的,没什么异常的地方,硬要说异常的地方的话如下:

 

          PushBackNode 参数给个假内存地址

          EraseNode参数给个假内存地址 或 内存根本不在链表中

 

         这些错的话可能来源于自己的粗心大意,写模板类时要非常注意,模板类出错有时候是看不出来的,所以需要先自己设一个假的模板类,比如把T想象成int,实现一次,没什么错,再弄成模板类。

 

         做到这里基本完成大半了,还有最后一步。

 

         4.定义内存池模板类:

 

         template<class T>

         class  CMemPool

         {

         public:

                  CMemPool();

                  ~CMemPool();

 

         public:

                   // 申请内存

                  T*              Alloc();

 

                   // 释放内存

                   VOID        Free(T* p);

 

         private:

                  CMemList<T> m_Retreat;                 // 未被占用内存链表

                  CMemList<T> m_Occupy;                // 已被占用内存链表

         };

 

        

         template<class T>

         inline CMemPool<T>::CMemPool()

         {

         }

 

         template<class T>

         inline CMemPool<T>::~CMemPool()

         {

                  SIZE_T              stIndex;

                  MemNode<T>*        pNode;

 

                  for (stIndex = m_Occupy.GetListCount(); stIndex > 0; --stIndex)

                  {

                           m_Occupy.PopBackNode(pNode);

                           ::HeapFree(::GetProcessHeap(), 0, pNode);

                  }

 

                  for (stIndex = m_Retreat.GetListCount(); stIndex > 0; --stIndex)

                  {

                           m_Retreat.PopBackNode(pNode);

                            ::HeapFree(::GetProcessHeap(), 0, pNode);

                  }

         }

 

         template<class T>

         inline T* CMemPool<T>::Alloc()

         {

                  if (0 == m_Retreat.GetListCount())

                  {

                            MemNode<T>* pNew = (MemNode<T>*)::HeapAlloc(::GetProcessHeap(), HEAP_ZERO_MEMORY, sizeof(MemNode<T>));

                           if (NULL == pNew) {

                                     return NULL;

                           }

 

                           m_Retreat.PushBackNode(pNew);

                           pNew->m_Type = NODE_RETREAT;

                  }

 

                  MemNode<T>* pAlloc(NULL);

 

                  m_Retreat.PopBackNode(pAlloc);

                  m_Occupy.PushBackNode(pAlloc);

                  pAlloc->m_Type = NODE_OCCUPY;

 

                  return pAlloc;

         }

 

         template<class T>

         inline VOID CMemPool<T>::Free(T* p)

         {

                  if (NODE_OCCUPY != ((MemNode<T>*)p)->m_Type) {

                           return;

                  }

 

                  m_Occupy.EraseNode((MemNode<T>*)p);

                  m_Retreat.PushBackNode((MemNode<T>*)p);

                  ((MemNode<T>*)p)->m_Type = NODE_RETREAT;

         }

 

         构造函数什么都不用做。

         析构函数负责把已占用和未被占用的内存全部释放掉,应该不会产生什么野指针。

 

         Alloc 负责申请内存,如果未被占用的内存链表中没有可重复使用的内存的话,就申请一个新的内存压进链表中,并把指针类型设为未被占用。然后从未被占用的内存链表末尾处移除出来,放入已被占用的内存链表中,并把指针的类型设为被占用,然后返回申请的内存指针。

         Free 负责释放内存,先判断申请的内存是不是已被占用的类型,如果不是,直接返回,避免重复压进链表。然后把申请内存从已被占用链表移除出来,放进未被占用的内存链表中,并设置内存类型为未被占用。这样就只是逻辑上的删除,真正内存还没释放,这样就可以重复利用了。

 

         就这样一个简单的内存池设计就弄好了。

 

         接下来就是总结问题了:

 

         1. 如果其他程序员粗心大意把内存越界操作了,就导致整个链表与内存池毁灭,最后就是程序奔溃。

 

         2. Free函数操作的参数 如果把不是 Alloc 申请的内存的话,可能造成越界,最后程序崩溃。

 

         3. 内存池是否需要设计一个峰值,如果申请的内存到了峰值,释放时不是压进未被占用的内存链表中,而是真正的释放内存?

 

         4. 我的这个设计是基于Windows 平台的,如果是Linux 的话可能需要改成 malloc 函数,不推荐使用new

 

         5. 是否需要改成像STL 库那样的内存分配器呢?

 

         6. 是否需要再做些什么改进?

 

         我一个人智商有限,能想到问题也就这么多了,只好借助 C++技术网 的力量了。

 

         最后感谢C++技术网的回答,也同时祝贺C++技术网及大家新年快乐!


C++技术网会员解答:

    此问题整理一下,是自己实现内存池的相关问题。前面描述的实现过程都没什么问题,所以在此做一个整体性的解答和建议。

    内存池是内存操作相对复杂的一个应用,如果操作不慎,就会引起严重问题。对于使用内存池时,建议要对内存池操作进行封装,隐藏内部操作细节,仅向上提供操作的上层API函数。这样可以规避使用内存池过程中带来的问题。内存池内部实现的机制要确保内存的正确使用。所以前两个问题就不再是问题。

    既然是内存池,就是要充分利用内存,避免频繁的内存申请和释放。内存池的大小是程序指定的,只是一块比较大的内存而已。并不是说,有了内存池就让系统内存可用量急剧下降。内存池只是来提高内存使用效率的,和内存池总大小没有直接关系。所以,申请到的内存池使用达到全部大小时,说明开始申请的内存大小已经不够用了。此时的不够用只是自己申请的内存池内存容量用完了,如果系统内存可用容量充足,那一般要继续申请新的内存加入到内存池中,确保内存池正常运转。一般情况系统内存都是充足的。如果系统内存不够用,也就是分配内存失败时,那么可以采取真正释放内存的策略。这就是内存释放的策略问题。或者说,内存池的大小的使用,关联到系统内存可用容量。这仅仅是一个策略问题。

    不过,内存池必须要有一个峰值。这个峰值就可以来决定是否要申请更多内存。这个峰值和内存释放没有关系。内存池总容量一定要高于峰值,峰值是执行内存管理策略的一个阈值。

    至于说分配和释放内存使用哪种方式,看设计的内存池应用的范围即可。

    内存池管理必须形成一个整体,只通过API对外开放。可以借鉴stl的内存管理。

    基本上是这样了,如果还有问题,请在文后留言。

    

文章来源:C++技术网 原创文章版权所有,未经授权,禁止转载。


当前位置:工作生活 -> 提问 -> [C++] 内存池架构设计问题

返回顶部

关于我们 QQ群 广告服务 增值服务 捐款资助 版权声明 RSS订阅 站点地图 百度网站地图 意见反馈
鄂ICP备14001349号-2, Copyright © 2014-2017, CJJJS.COM/CJJJS.CN, All Rights Reserved

在线提问
问题标题:
问题描述:(简陋的描述会导致问题被最后回答、没有针对性回答甚至无法解答。请确保问题描述的足够清楚。)