高并发内存池---实现中心缓存
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);