【项目日记】高并发内存池---实现页缓存
放纵自己的欲望是最大的祸害;
谈论别人的隐私是最大的罪恶;
不知自己过失是最大的病痛。
--- 亚里士多德 ---
高并发内存池---实现页缓存
- 1 页缓存整体设计思路
- 2 框架搭建
- 3 NewSpan函数
- 4 请求Span联动
1 页缓存整体设计思路
首先我们来看页缓存的设计思路,明白思路,代码就可以更加舒畅的写出来,并且这个项目的调试比较困难,一定一定要仔细明白设计思路,把代码仔细写好才能保证我们的开发效率!
页缓存是所有线程共享的,当线程缓存内部的_freelist中没有内存块时,会向中心缓存进行申请。中心缓存内部的_spanlist如果span中还有内存块就可以进行返回,如果没有就要去页缓存进行Span的申请。页缓存全局只有一个!
- 页缓存需要设计为单例模式!这里使用懒汉模式实现!
页缓存内部与线程缓存和中心缓存不一样,内部不是通过内存大小进行的映射,而是通过页的数量来进行映射。每次中心缓存申请是时候会标明申请的页数,进而找到对应页的链表,查看是否有可以使用的span,如果没有,就向页数大的链表查找,如果有就进行拆分!
- 页缓存内部存在以页数作为映射的链表数组!
- 页缓存内部的申请机制是先在对应页数的链表中进行查找,命中就直接进行返回。没有有命中就向页数大的链表中进行查找,命中就进行拆分。如果没有命中,就创建一个最大页数的span,挂载到对应链表上!
2 框架搭建
现在我们来通过单例模式搭建其基础的框架:
- 类内设置唯一静态对象,注意要在类外进行定义!
- 设置
GetInstance()
,可以获取唯一静态对象,在第一次获取时进行创建! - 注意需要单例锁来进行保护,因为第一次获取时很有可能会让两个线程同时进来,就会导致错误!
class PageCache
{
public:
//懒汉模式进行时间
static PageCache* GetInstance()
{
if (_pinfo == nullptr)
{
std::unique_lock<std::mutex> lock(_single_mtx);
if (_pinfo == nullptr)
{
_pinfo = new PageCache();
}
}
return _pinfo;
}
//给中心缓存提供span的接口
Span* NewSpan(size_t num);
private:
//根据span的页的个数进行分类
SpanList _pageList[PAGENUM]; // 1 - 128
//页锁
std::mutex _page_mtx;
//单例模式设计
private:
static PageCache* _pinfo; //需要类外声明
static std::mutex _single_mtx; //单例锁
//构造函数私有化
PageCache() {};
//拷贝构造 赋值重载 删除
PageCache(const PageCache&) = delete;
PageCache operator=(const PageCache&) = delete;
};
3 NewSpan函数
NewSpan
函数通过中心缓存提供的页数来进行查找:
- 首先先在num对应的链表中查找是否有可以使用的span。
- 如果没有就向页数大的链表中进行查找,如果找到就进行拆分,分成两个span,一个用来返回,另一个再重新插入到对应链表中
- 如果没有就创建一个大span,挂载到最大页数对应的链表中,然后再次返回调用NewSpan进行递归即可!
Span* PageCache::NewSpan(size_t num)
{
// 1 - 128
assert(num > 0 && num < PAGENUM);
//先在当前页对应的链表中进行寻找,没有就向后寻找
if (!_pageList[num].Empty())
{
//不为空就直接返回一个span对象
return _pageList->PopFront();
}
//如果没有 遍历后续的链表 查看有没有更大的span
else
{
for (int i = num + 1; i < PAGENUM; i++)
{
if (!_pageList[i].Empty())
{
//不为空 说明存在页数为 i 的 span
//进行拆分
Span* kspan = new Span;
Span* nspan = _pageList[i].PopFront();
//kspan 的赋值
kspan->_n = num;
kspan->_pageid = nspan->_pageid;
nspan->_n -= num;
nspan->_pageid += num;
_pageList[nspan->_n].PushFront(nspan);
return kspan;
}
}
}
//到了这里就说明整个_pagelist中没有可以使用的span , 需要申请一个大span
Span* bigspan = new Span;
//申请大空间
void* ptr = SystemAlloc(PAGENUM - 1);
//通过ptr初始化newspan
bigspan->_n = PAGENUM - 1;
bigspan->_pageid = (PAGE_ID)ptr >> PAGE_SHIFT;
//插入到_pagelist中
_pageList[PAGENUM - 1].PushFront(bigspan);
//递归调用
return NewSpan(num);
}
注意NewSpan
函数是对临界区的操作,需要进行加锁,但是这里设计到了递归调用函数,在函数内使用普通的互斥锁会造成死锁,对于这个问题一般有三种解决方案:
- 使用递归锁
recursive_mutex
,就可以避免出现递归造成的死锁问题! - 设置子函数,我们函数
NewSpan
中进行上锁,然后调用子函数_NewSpan
进行操作! - 在进入
NewSpan
之前就上好锁,结束退出之后再解锁,类似子函数的解决方案!
这里还有加入对应的内存开辟函数,使用条件编译,使其适配所有的操作系统!
#ifdef _WIN32
#define NOMINMAX//避免和std中的min max冲突!
#include<windows.h>
#else
// linux
#endif
// --- 直接去堆上按页申请空间 ---
inline static void* SystemAlloc(size_t kpage)
{
#ifdef _WIN32
void* ptr = VirtualAlloc(0, kpage << 13, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
#else
// linux下brk mmap等
#endif
if (ptr == nullptr)
throw std::bad_alloc();
return ptr;
}
4 请求Span联动
我们回到中心缓存的GetOneSpan
函数中,线程缓存向这里索要span,中心缓存给不出就去页缓存申请Span:
- 首先先在中心缓存中的
spanlist
中进行查找,如果有就直接进行返回 - 没有span就去页缓存中进行申请,注意这里要把
list
桶锁解掉,因为操作会去到页缓存,离开list
临界区。如果继续上锁会影响释放内存的操作! - 从页缓存中获取到
span
就进行处理:- 首先通过
span
的_pageid
确定地址区间,然后通过start
和end
两个指针将span内部的大内存块拆分成小内存块尾插到span->_freelist
中。尾插物理空间连续, 缓存利用率高! - 最后将span插入到链表中,并将其返回!线程缓存会对
span
的_freelist
中的内存块进行操作!
- 首先通过
Span* CentralCache::GetOneSpan(SpanList& list, size_t size)
{
//...等待联动...
//向spanlist中寻找是否有span可以使用
Span* it = list.Begin();
//找到挂载着内存块的span!
if (it != list.End())
{
if (it->_freelist != nullptr)
{
return it;
}
else
{
it = it->_next;
}
}
//到这里说明spanlist中没有span对象
//没有就向页缓存申请
//1. 需要计算需要申请几个页! SizeClass::NumMovePage
size_t num = SizeClass::NumMovePage(size);
list.GetMutex().unlock();//将桶锁解开 避免影响该链表是内存的释放
//根据页数申请span
PageCache::GetInstance()->GetMuetx().lock(); // 上页锁
Span* span = PageCache::GetInstance()->NewSpan(num);
PageCache::GetInstance()->GetMuetx().unlock();
//不需要立刻恢复桶锁 到临界区在上锁就可以
//获取到span之后进行处理
//通过获取的span内部的属性确定其映射的地址区间!
//通过span的页号确定span的起始地址
char* start = (char*)(span->_pageid << PAGE_SHIFT);
char* end = start + (num << PAGE_SHIFT);//页数确定内存字节数大小
//把大块内存切好挂接起来 --- 最好使用尾插 物理空间连续 缓存利用率高
span->_freelist = start;
start += size;
void* tail = span->_freelist;
while (start < end)
{
NextObj(tail) = start;
tail = start; //tail = NextObj(tail)
start += size;
}
//放入spanlist中 需要加锁
list.GetMutex().lock();
list.PushFront(span);
return span;
//return nullptr;
}
这样就形成闭环,我们的申请大致的框架就已经完了,下一篇文章我们来进行申请联调的测试!!!