高并发内存池---实现中心缓存
1 整体理念
实现中心缓存之前,我们先理解中心缓存需要做那些事情,具有哪些特性?我们把中心缓存的功能特性理解清楚了自然而然的就可以写出代码来!
首先,中心缓存和线程缓存不一样,不是每个线程都拥有自己的独一缓存,而是每个线程都可以进行访问,可以向中心缓存发出请求!所以中心缓存最好也必须只有一个,所有的线程都去这唯一一个中心缓存中进请求和释放内存块,效率更高,逻辑更加清晰!!!
- 中心缓存应该按照单例模式进行设计,可以使用懒汉模型也可以使用饿汉模型!
中心缓存中,也有一个类似哈希表的结构,其中储存的不是内存块,而是一种特别的对象
span
(以页为单位的大块内存),这里面保护一个自由链表。也就是说span是一个大内存块,里面有很多小内存块供线程缓存取用。为了适配线程缓存的自由链表数,中心缓存采取同样的对齐映射规则,依旧是208个链表!
- 中心缓存中有一个
spanlist
数组 ,每个链表中储存若干个span
,span
中有一个自由链表,可以取出对应大小的内存块供线程缓存进行使用。这里的spanlist采取带头双向循环链表,让操作更加灵活!
线程缓存向中心缓存请求内存块是一个高并发场景,会存在线程安全的问题!所以需要加入锁,但是一般的互斥锁会直接锁住整个中心缓存,会造成性能的极大下降,因为每次请求的内存块只会在一个``spanlist`中,不会影响其他的链表,所以这里使用桶锁更加合适,只锁住一个桶,而不影响中心缓存中其他链表的请求!
spanlist
中需要加入桶锁,请求内存时要锁住,保证线程安全!!!
在实际场景中线程缓存会不断的向中心缓存请求内存块,如果一次只给一块,那么就会导致频繁的请求加锁,导致性能变慢!为高并发内存池性能的关键是线程缓存中储存的内存块,我们一次多給一些,就能保证效率!同时为了保证内存最大限度的合理使用,我们采取慢调度开始算法,让线程申请的内存块逐渐增加,而不是一下子就给很多!!!
- 需要加入慢调度开始算法,保证效率!
2 SpanList的实现
我们先来实现最底层的span
:
- 要支持双向链表,内部加入前后指针
- 加入页号和页的数量,便于分辨与统计
- 加入引用计数,用来判断span是否可以进行回收
- 内部有一个自由链表,储存切分好的内存块!
struct Span
{
//起始页的页号
PAGE_ID _pageid = 0;
//页的数量
size_t n = 0;
//双向链表结构
Span* _next = nullptr;
Span* _prev = nullptr;
//引用计数
size_t _usecount = 0; //被请求 ++ 被归还-- 为0时可以进行整合
//被切分好的对象的大小
size_t _objsize = 0;
//储存切分好的对象的自由链表
void* _freelist = nullptr;
//是否被使用
bool _isuse = false;
};
基于这个span
,我们来实现一个带头双向链表:
- 为了方便头插和尾插,我们直接写一个万能的插入函数
- 再写一个删除函数
- 加入桶锁,一定一定要加入桶锁!并写一个获取锁的接口!
class SpanList
{
public:
//构造函数
SpanList()
{
_head = new Span;
_head->_prev = _head;
_head->_next = _head;
}
// --- 迭代器 ---
//插入节点
void Insert(Span* pos , Span* newspan)
{
assert(pos);
assert(newspan);
//插入到pos前面
Span* prev = pos->_prev;
//进行插入
prev->_next = newspan;
newspan->_prev = prev;
newspan->_next = pos;
pos->_prev = newspan;
}
//删除节点
void Erease(Span *pos)
{
assert(pos);
//将pos节点删除
Span* prev = pos->_prev;
Span* next = pos->_next;
prev->_next = next;
next->_prev = prev;
}
//获取锁!
std::mutex& GetMutex()
{
return _mtx;
}
private:
//头节点
Span* _head;
//加入桶锁
std::mutex _mtx;
};
3 CentralCache的实现
我们先搭建起一个框架:
//所有线程共用一个中心缓存
class CentralCache
{
public:
//用来为线程缓存提供内存块
size_t FetchRangeobj(void* start, void* end, size_t batchnum, size_t size);
//向页缓存获取 span 对象
Span* GetOneSpan(SpanList& list, size_t size);
private:
//span链表数组 与 线程缓存的自由链表数组一一对应
SpanList _spanlists[LISTNUM];
};
其中函数GetOneSpan
先不需要管,等我们实现页缓存再来进行联动!!!
FetchRangeobj
函数一会来实现,我们先把上层线程缓存的联动调用写好!!!
现在我们先把CentralCache
设计为单例模式,这里采取懒汉模式进行处理!
- 注意:类内静态变量一定要在类外进行定义声明
- 首先我们在类内加入一个静态变量,整个类都只能有这一个对象!
- 然后为了保证“整个类都只能有这一个对象”,需要把构造函数进行私有化,并且删除拷贝构造和赋值拷贝
- 最后提供一个接口,保证每次调用时可以获取到这唯一的一个类对象!在第一次调用时进行创建,这里需要加锁保护,因为调用可以在不同线程内同时进行,要保证线程安全!
//所有线程共用一个中心缓存
//所以按照单例模式 --- 懒汉模型来进行设计
class CentralCache
{
public:
static CentralCache* GetInstance()
{
双检查提高性能
if (_pinfo == nullptr)
{
//上锁
std::unique_lock<std::mutex> lock(_single_mtx);
if (_pinfo == nullptr)
{
_pinfo = new CentralCache;
}
}
//_pinfo = new CentralCache;
return _pinfo;
}
size_t FetchRangeobj(void* start, void* end, size_t batchnum, size_t size);
Span* GetOneSpan(SpanList& list, size_t size);
private:
//span链表数组 与 线程缓存的自由链表数组一一对应
SpanList _spanlists[LISTNUM];
private :
//单例模式设计
//构造函数私有化
CentralCache() {}
//拷贝构造 赋值重载 Delete
CentralCache operator=(const CentralCache& ) = delete;
CentralCache(const CentralCache&) = delete;
//唯一静态对象
static CentralCache* _pinfo;
//锁对象来保证单例模式创建时的线程安全
static std::mutex _single_mtx;
};
4 请求内存联动
接下来我们就来完成线程缓存向中心缓存请求内存的接口。
首先我们先来写上层的线程缓存的接口FetchFromCentralCache
,我们需要做到以下工作:
- 确定申请的个数:采取慢开始反馈调节算法,让请求的内存块逐渐增加,并且要注意有一个上限,这个上限按照申请的大小反比例对应
- 向中心缓存进行请求:创建两个变量(开始节点和结束节点),通过接口的输出型参数获取,可以获取到一段内存块链表,注意此时要获取实际的请求个数,因为span中可能不够了。
- 返回内存块:将请求获取的若干个内存块中返回一个,其余的储存在对应的自由链表中!!!
//按照字节数反比例返回内存块数量
static size_t SizeClass::NumMoveSize(size_t size)
{
assert(size < MAX_BYTES);
if (size == 0) return 0;
//范围在 [2 , 512]
int num = MAX_BYTES / size;
//保证范围
if (num < 2)
{
return 2;
}
else if (num > 512)
{
return 512;
}
else
return num;
}
void* ThreadCache::FetchFromCentralCache(size_t index, size_t alignSize)
{
// 慢开始反馈调节算法
// 1、最开始不会一次向centralcache一次批量要太多,因为要太多了可能用不完
// 2、如果你不要这个size大小内存需求,那么batchNum就会不断增长,直到上限
// 3、size越大,一次向central cache要的batchNum就越小
// 4、size越小,一次向central cache要的batchNum就越大
//根据自由链表的Maxsize比较大小 得到这一批的数量
//如果是批数 == Maxsize , 那Maxsize增加;
int batchNum = std::min( _freelist[index].MaxSize(), SizeClass::NumMoveSize(alignSize) );
if (batchNum == _freelist[index].MaxSize())
{
//慢调节
_freelist[index].MaxSize() += 1;
}
//向中心缓存申请内存
void* start = nullptr; // 开始节点的地址
void* end = nullptr; // 结束节点的地址
//获取唯一的单例对象进行申请 FetchRangeobj()
CentralCache* pcc = CentralCache::GetInstance();
//申请不一定有这么多 , 需要获得实际个数
size_t actualnum = pcc->FetchRangeobj(start, end, batchNum, alignSize);
assert(actualnum > 1);
//如果只有一个直接返回就可以
if (actualnum == 1)
{
assert(start == end);
return start;
}
//有多个就需要把多余的内存块插入到自由链表中
else
{
_freelist[index].PushRange(NextObj(start), end);
return start;
}
}
接下来就来实现中心缓存的发送内存块:
- 首先,中心缓存是临界区,找到对应链表后要及时加锁,这里采取RAII形式的锁守卫,方便使用
- 然后获取一个
span
对象,等待与页缓存联动 - 在
span
对象中的自由链表中进行选取对应数量的内存块,注意不能超出span
的范围!!
//给Threadcache提供内存块
size_t CentralCache::FetchRangeobj(void* start, void* end, size_t batchnum, size_t size)
{
//找到是哪一个桶
size_t index = SizeClass::Index(size);
//加减锁
std::unique_lock<std::mutex> lock(_spanlists[index].GetMutex());
//获取一个span对象
Span* span = GetOneSpan(_spanlists[index], size);
assert(span);
assert(span->_freelist);
//对span对象中的链表进行选取
start = span->_freelist;
end = start;
//读取batch个内存块,并且保证不超出范围
size_t i = 0;
size_t actualnum = 1;
while (i < batchnum - 1 && NextObj(end) != nullptr)
{
end = NextObj(end);
i++;
actualnum++;
}
span->_freelist = NextObj(end);
//处理最后的节点
NextObj(end) = nullptr;
//RAII规则下的锁守卫会自动释放!
return actualnum;
}
这样线程缓存的请求内存块的联动就完成了!!!
后续我们来完成页缓存的结构,然后联动起来,将请求内存的联动写好!!!