【项目日记】高并发内存池---实现页缓存

[复制链接]
发表于 2026-1-27 15:19:05 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×

  放纵本身的欲望是最大的祸患;    评论别人的隐私是最大的罪过;      不知本身不对是最大的病痛。        --- 亚里士多德 ---       


1 页缓存团体计划思绪

起首我们来看页缓存的计划思绪,明白思绪,代码就可以更加愉快的写出来,而且这个项目标调试比力困难,肯定肯定要过细明白计划思绪,把代码过细写好才气包管我们的开发服从!
   页缓存是全部线程共享的,当线程缓存内部的_freelist中没有内存块时,会向中心缓存举行申请。中心缓存内部的_spanlist如果span中尚有内存块就可以举行返回,如果没有就要去页缓存举行Span的申请。页缓存全局只有一个!
  

  • 页缓存必要计划为单例模式!这里利用懒汉模式实现!
   页缓存内部与线程缓存和中心缓存不一样,内部不是通过内存巨细举行的映射,而是通过页的数目来举行映射。每次中心缓存申请是时间会标明申请的页数,进而找到对应页的链表,查察是否有可以利用的span,如果没有,就向页数大的链表查找,如果有就举行拆分!
  

  • 页缓存内部存在以页数作为映射的链表数组!
  • 页缓存内部的申请机制是先在对应页数的链表中举行查找,掷中就直接举行返回。没有有掷中就向页数大的链表中举行查找,掷中就举行拆分。如果没有掷中,就创建一个最大页数的span,挂载到对应链表上!
2 框架搭建

现在我们来通过单例模式搭建其根本的框架:

  • 类内设置唯一静态对象,留意要在类外举行界说!
  • 设置GetInstance(),可以获取唯一静态对象,在第一次获取时举行创建!
  • 留意必要单例锁来举行掩护,由于第一次获取时很有大概会让两个线程同时进来,就会导致错误!
  1. class PageCache
  2. {
  3. public:
  4.         //懒汉模式进行时间
  5.         static PageCache* GetInstance()
  6.         {
  7.                 if (_pinfo == nullptr)
  8.                 {
  9.                         std::unique_lock<std::mutex> lock(_single_mtx);
  10.                         if (_pinfo == nullptr)
  11.                         {
  12.                                 _pinfo = new PageCache();
  13.                         }
  14.                 }
  15.                 return _pinfo;
  16.         }
  17.         //给中心缓存提供span的接口
  18.         Span* NewSpan(size_t num);
  19. private:
  20.         //根据span的页的个数进行分类
  21.         SpanList _pageList[PAGENUM]; // 1 - 128
  22.         //页锁
  23.         std::mutex _page_mtx;
  24. //单例模式设计
  25. private:
  26.         static PageCache* _pinfo;                //需要类外声明
  27.         static std::mutex _single_mtx;        //单例锁
  28.         //构造函数私有化
  29.         PageCache() {};
  30.         //拷贝构造 赋值重载 删除
  31.         PageCache(const PageCache&) = delete;
  32.         PageCache operator=(const PageCache&) = delete;
  33. };
复制代码
3 NewSpan函数

NewSpan函数通过中心缓存提供的页数来举行查找:

  • 起首先在num对应的链表中查找是否有可以利用的span。
  • 如果没有就向页数大的链表中举行查找,如果找到就举行拆分,分成两个span,一个用来返回,另一个再重新插入到对应链表中
  • 如果没有就创建一个大span,挂载到最大页数对应的链表中,然后再次返回调用NewSpan举行递归即可!
  1. Span* PageCache::NewSpan(size_t num)
  2. {
  3.         // 1 - 128
  4.         assert(num > 0 && num < PAGENUM);
  5.         //先在当前页对应的链表中进行寻找,没有就向后寻找
  6.         if (!_pageList[num].Empty())
  7.         {
  8.                 //不为空就直接返回一个span对象
  9.                 return _pageList->PopFront();
  10.         }
  11.         //如果没有 遍历后续的链表 查看有没有更大的span
  12.         else
  13.         {
  14.                 for (int i = num + 1; i < PAGENUM; i++)
  15.                 {
  16.                         if (!_pageList[i].Empty())
  17.                         {
  18.                                 //不为空 说明存在页数为 i 的 span
  19.                                 //进行拆分
  20.                                 Span* kspan = new Span;
  21.                                 Span* nspan = _pageList[i].PopFront();
  22.                                 //kspan 的赋值
  23.                                 kspan->_n = num;
  24.                                 kspan->_pageid = nspan->_pageid;
  25.                                 nspan->_n -= num;
  26.                                 nspan->_pageid += num;
  27.                                 _pageList[nspan->_n].PushFront(nspan);
  28.                                 return kspan;
  29.                         }
  30.                 }
  31.         }
  32.         //到了这里就说明整个_pagelist中没有可以使用的span , 需要申请一个大span
  33.         Span* bigspan = new Span;
  34.         //申请大空间
  35.         void* ptr = SystemAlloc(PAGENUM - 1);
  36.         //通过ptr初始化newspan
  37.         bigspan->_n = PAGENUM - 1;
  38.         bigspan->_pageid = (PAGE_ID)ptr >> PAGE_SHIFT;
  39.        
  40.         //插入到_pagelist中
  41.         _pageList[PAGENUM - 1].PushFront(bigspan);
  42.         //递归调用
  43.         return NewSpan(num);
  44. }
复制代码
留意NewSpan函数是对临界区的利用,必要举行加锁,但是这里计划到了递归调用函数,在函数内利用平凡的互斥锁会造成死锁,对于这个标题一样寻常有三种办理方案:

  • 利用递归锁recursive_mutex,就可以克制出现递归造成的死锁标题!
  • 设置子函数,我们函数NewSpan中举行上锁,然后调用子函数_NewSpan举行利用!
  • 在进入NewSpan之前就上好锁,竣事退出之后再解锁,雷同子函数的办理方案!
这里尚有到场对应的内存开发函数,利用条件编译,使其适配全部的利用体系!
  1. #ifdef _WIN32
  2. #define NOMINMAX//避免和std中的min max冲突!
  3. #include<windows.h>
  4. #else
  5. // linux
  6. #endif
  7. // --- 直接去堆上按页申请空间 ---
  8. inline static void* SystemAlloc(size_t kpage)
  9. {
  10. #ifdef _WIN32
  11.         void* ptr = VirtualAlloc(0, kpage << 13, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
  12. #else
  13.         // linux下brk mmap等
  14. #endif
  15.         if (ptr == nullptr)
  16.                 throw std::bad_alloc();
  17.         return ptr;
  18. }
复制代码
4 哀求Span联动

我们回到中心缓存的GetOneSpan 函数中,线程缓存向这里索要span,中心缓存给不出就去页缓存申请Span:

  • 起首先在中心缓存中的spanlist中举行查找,如果有就直接举行返回
  • 没有span就去页缓存中举行申请,留意这里要把list桶锁解掉,由于利用会去到页缓存,离开list临界区。如果继续上锁会影响开释内存的利用!
  • 从页缓存中获取到span就举行处置惩罚:

    • 起首通过span的_pageid确定地点区间,然后通过start和end两个指针将span内部的大内存块拆分成小内存块尾插到span->_freelist中。尾插物理空间一连, 缓存利用率高!
    • 末了将span插入到链表中,并将其返回!线程缓存会对span的_freelist中的内存块举行利用!

  1. Span* CentralCache::GetOneSpan(SpanList& list, size_t size)
  2. {
  3.         //...等待联动...
  4.         //向spanlist中寻找是否有span可以使用
  5.         Span* it = list.Begin();
  6.         //找到挂载着内存块的span!
  7.         if (it != list.End())
  8.         {
  9.                 if (it->_freelist != nullptr)
  10.                 {
  11.                         return it;
  12.                 }
  13.                 else
  14.                 {
  15.                         it = it->_next;
  16.                 }
  17.         }
  18.         //到这里说明spanlist中没有span对象
  19.         //没有就向页缓存申请
  20.         //1. 需要计算需要申请几个页! SizeClass::NumMovePage
  21.         size_t num = SizeClass::NumMovePage(size);
  22.         list.GetMutex().unlock();//将桶锁解开  避免影响该链表是内存的释放
  23.         //根据页数申请span
  24.         PageCache::GetInstance()->GetMuetx().lock(); // 上页锁
  25.         Span* span = PageCache::GetInstance()->NewSpan(num);
  26.         PageCache::GetInstance()->GetMuetx().unlock();
  27.         //不需要立刻恢复桶锁 到临界区在上锁就可以
  28.         //获取到span之后进行处理
  29.         //通过获取的span内部的属性确定其映射的地址区间!
  30.         //通过span的页号确定span的起始地址
  31.         char* start = (char*)(span->_pageid << PAGE_SHIFT);
  32.         char* end = start + (num << PAGE_SHIFT);//页数确定内存字节数大小
  33.         //把大块内存切好挂接起来 --- 最好使用尾插 物理空间连续 缓存利用率高
  34.         span->_freelist = start;
  35.         start += size;
  36.         void* tail = span->_freelist;
  37.         while (start < end)
  38.         {
  39.                 NextObj(tail) = start;
  40.                 tail = start; //tail = NextObj(tail)
  41.                 start += size;
  42.         }
  43.         //放入spanlist中 需要加锁
  44.         list.GetMutex().lock();
  45.         list.PushFront(span);
  46.         return span;
  47.         //return nullptr;
  48. }
复制代码
如许就形成闭环,我们的申请大抵的框架就已经完了,下一篇文章我们来举行申请联调的测试!!!
回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表