1.什么是内存池?
1.1 池化技术
池 是在计算机技术中经常使用的一种设计模式,它是将程序中需要经常使用的核心资源先申请出来,放到一个池当中,这部分资源由程序自己去进行管理,通过以这样的方式来提高资源的利用率,也可以保证本程序占有的资源数量。经常使用的池技术包括内存池,线程池和连接池等,其中内存池和线程池使用最多。
1.2 内存池
内存池(Memory Pool)是一种动态内存分配与管理技术。通常的情况下,对于动态的分配和释放内存是通过new,delete,malloc,free等API(程序之间的接口),这样导致的问题是:
1.当频繁且多次的去申请内存时会消耗不少的时间从而降低效率,因为当使用mmap还分配内存 时,向操作系统申请内存这个申请的动作是要通过系统调用,而执行系统调用是要进入内核态 的,然后再回到用户态,运行态的切换会消耗不少的时间。(new底层也会去调用malloc)
(malloc申请内存的时候,会有两种方式向操作系统申请堆内存,方式1:通过brk()系统调用从堆 分配内存,方式2:通过mmap()系统调用在文件映射区域分配内存)
2.当频繁的使用malloc和free时,会造成大量的内存碎片,例如当我连续申请了10k,5k,20k,当 10k,20k的空间用完后free掉了,当我此时想申请30k的空间时,此时这10k和20k是直接用不了 的,因为它们不是连续的,此时就只能在向OS提出申请,此时实际使用内存继续增加。
3.malloc不是线程安全的
当面对这些问题时,对于线程池面对同样的情况它的做法是,在真正使用内存之前,先申请分配一大块内存(内存池)留作备用,当进行申请内存时,从池中取出一块动态分配,当释放内存时,将释放的内存再放入池内,再次申请池可以再取出来使用,并尽量与周边的空闲内存块合并,若内存池不够时,则自动扩大内存池,从操作系统中申请更大的内存池。
2.为什么需要内存池?
2.1 内存碎片问题
假设系统依次分配了出去了16byte,8byte,16byte,4byte,而8byte和4byte的空间还了回来,但是此时要分配一个11byte的空间,此时8byte和4byte这2块空间之和有12byte,但是无法分配出一个连续11byte的空间,当假如后续申请堆空间的需求始终是>8byte的,那么此时这个8byte的空间就是内存碎片。
2.2 申请效率的问题
例如:我要问妈妈要钱,每次要5元~30元,你和你妈妈之间的距离是1公里
方式1:我每次要多少妈妈就给多少
方式2:我第一次问的时候,妈妈直接给了我100元
此时对比上面2种方式,明显是第2种方式效率要更高。其实申请内存也是同样的,在频繁的申请内存的场景下,用方式2效率更高。
3.内存池的设计
3.1 定长内存池
3.1.1 设计的逻辑
_memory指针对这块空间进行管理。
_remainBytes用来记录申请的这块大块内存中剩余空间的大小,由它去判断这块大块的内存是否已经用完了。
_freeList 表示当把这一大块的内存切为小的内存块给其他的线程去使用后还了回来此时我用一个_freeList指针去作为头指针去管理一个链表,这个链表把所有还来的内存块串接在一起进行管理。(单向不带头不循环链表)
_freeList指向的链式结构用来管理还回来的内存块,_memory这个指针指向你预先向堆申请的空间,这块空间就是内存池当中的内存资源,未来要对_memory指向的这块内存空间进行切分出去内存块给申请的对象去进行使用,当申请的对象把申请的内存块不用了以后还了回来,我在用_freeList这个链表去管理释放回来的内存块,这个链式结构管理的内存块未来可以进行重复利用,即:重新分配给申请的对象使用。
3.1.2 头文件部分
#include<windows.h>
#include<iostream>
#include<new>
#include<memoryapi.h>
#include<vector>
#include<time.h>
3.1.3 向堆申请
inline static void* SystemAlloc(size_t kpage)
{
#ifdef _WIN32
void* ptr = VirtualAlloc(0, kpage << 13, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
#else
//Linux下
#endif
if (ptr == nullptr)
throw std::bad_alloc();
return ptr;
}
3.1.4 定长池
template<class T>
class ObjectPool
{
public:
T* New()
{
T* obj = nullptr;
//当链表不为空时,就优先把链表中的内存块给申请方使用
if (_freeList)
{
void* next = *((void**)_freeList);
obj = (T*)_freeList;
_freeList = next;
}
//当链表为空时,就向内存池当中的内存资源申请内存块
else
{
//_remainBytes表示的内存池当中剩余空间的大小,当小于我一次申请的sizeof(T)时就
//向堆当中申请资源
if (_remainBytes < sizeof(T))
{
_remainBytes = 128 * 1024;//申请的Byte大小
//SystemAlloc是以页为单位进行申请的
_memory = (char*)SystemAlloc(_remainBytes >> 13); //_memory管理的是以页
//判断是否申请失败 //为单位的空间
if (_memory == nullptr)
{
throw std::bad_alloc();
}
}
//向_memory管理的内存池资源申请内存
obj = (T*)_memory;
size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);
_memory += objSize;
_remainBytes -= objSize;
}
//定位new,显示调用T的构造函数初始化
new(obj)T;
return obj;
}
//管理内存块
void Delete(T* obj)
{
//使用定位new之后,释放对象
obj->~T();
//头插,把不用的内存块用_freeList指向的链表管理起来
*(void**)obj = _freeList;
_freeList = obj;
}
private:
char* _memory = nullptr;
size_t _remainBytes = 0;
void* _freeList = nullptr;
};
3.2 并发内存池concurrent memory pool
现在的很多开发环境都是多核多线程的,因此在申请内存的场景下,必然存在激烈的锁竞争问题,所以在实现的时候内存池要考虑以下几个方面的问题:1.内存碎片问题。 2.性能问题。 3.多核多线程环境下,锁竞争问题。
concurrent memory pool主要是由下面的3个部分构成:
1.thread cache:线程缓存是每个线程独有的,用于小于64k的内存分配,线程在这里申请内存不需要加锁,每一个线程独享一个cache。
2.central cache:中心缓存是所有线程所共享,thread cache是按照需求从central cache中获取内存对象。central cache是周期性的回收thread cache中的对象,避免一个线程占用过多的内存从而导致其他的线程可使用的内存资源不足,central cache同时也会回收thread cache然后在把回收的内存保存起来供其他的线程去申请然后使用。它可以达到使内存分配在多个线程中更均衡的按需调度的目的。central cache也是存在竞争的,因此存取对象是需要加锁的,但是锁采用的是桶锁,因此在多线程的场景下竞争资源是发生在桶当中的。
3.page cache:页缓存是在central cache缓存上面的一层缓存,存储内存是以页为单位存储及分布的,centralcache没内存对象时,从page cache分配出一定数量的page,并按照条件切为指定大小的内存,分配给central cache。page cache会回收central cache满足条件的span对象,并且和相邻的页合并为更大的页,缓解内存碎片问题。
3.2.1 Common.h 部分
#pragma once//防止这个文件被重复的包含
#include<algorithm>//这个里面有函数模板min
#ifdef _WIN32
#include<windows.h>//这个里面有一个min定义的宏
#else
//Linux下的
#endif
#include<iostream>
#include<vector>
#include<time.h>
#include<assert.h>
#include<thread>
#include<mutex>
#include<memoryapi.h>
#include<unordered_map>
#include<algorithm>
using std::cout;
using std::endl;
static const size_t MAX_BYTES = 256 * 1024; //256(最大的桶)KB * 1024 = 字节数 (1KB = 1024B 1个页是4KB 1byte(字节)就是1B 字节就是Byte,也是B)
static const size_t NFREELIST = 208;//总的哈希桶的数量
static const size_t NPAGES = 129;//0~129 0空出来,只取1~129这128个
static const size_t PAGE_SHIFT = 13;
//在x64配置下,_WIN32和_WIN64都有定义
#ifdef _WIN64
typedef unsigned long long PAGE_ID;
#elif _WIN32
typedef size_t PAGE_ID;
#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;
}
inline static void SystemFree(void* ptr)
{
#ifdef _WIN32
VirtualFree(ptr, 0, MEM_RELEASE);
#else
//Linux下
#endif
}
//取内存块的前4个或前8个字节去保存下一个节点的地址
static void*& NextObj(void* obj)//设置为static,保证唯一性
{
return *(void**)obj;
}
//管理切分好的小对象的自由链表
class FreeList
{
public:
void Push(void* obj)//obj指向要插入的节点
{
assert(obj);
//头插
//*(void**)obj = _freeList;
NextObj(obj) = _freeList;
_freeList = obj;
++_size;
}
//malloc的返回值是申请空间的起始地址
void* Pop()//void* 一个任意类型的指针
{
assert(_freeList);
//头删
void* obj = _freeList;
_freeList = NextObj(obj);
--_size;
return obj;
}
//把从centralcache申请到的内存挂到threadcache对应的桶上
//只有当threadcache当中的桶为空时,才会向centralcache当中去申请,此时从centralcache获取到
//的一段内存是要挂到threadcache的空桶上的
void PushRange(void* start, void* end,size_t n)
{
NextObj(end) = _freeList;
_freeList = start;
_size += n;
}
void PopRange(void*& start, void*& end, size_t n)
{
assert(n >= _size);
start = _freeList;
end = start;
for (size_t i = 0; i < n - 1; ++i)
{
end = NextObj(end);
}
_freeList = NextObj(end);
NextObj(end) = nullptr;
_size -= n;
}
bool Empty()
{
return _freeList == nullptr;//_freeList指向nullptr则这个桶为空
}
size_t& MaxSize()
{
return _maxSize;
}
size_t Size()
{
return _size;
}
private:
void* _freeList = nullptr;//指向链表头部的指针
size_t _maxSize = 1;//NumMoveSize在计算给的大块内存的大小时,假如是在上下限之间的,你要8个
//我给了512个,这样太浪费了
size_t _size = 0;
};
//计算对象大小的对齐映射规则
class SizeClass
{
public:
// 控制在10%左右的内碎片浪费
// [0+1,128] 8byte对齐 freelist[0,16) [1,128]按8字节去对齐总共就16个桶 0到15 128/8=16个桶 这16个桶都按照8字节去对齐
// [128+1,1024] 16byte对齐 freelist[16,72) 129+15(按16对齐)=144 128+1byte到1024byte 这个范围内都按照16byte去进行申请,会用56个桶 1024-128=896 896/16=56个桶
// [1024+1,8*1024] 128byte对齐 freelist[72,128) 1025+127=1152(对齐后) 我给你开了1152而我实际只要1027/1152 = 0.1 10% 浪费了10%左右
// [8*1024+1,64*1024] 1024byte对齐 freelist[128,184) 8*1024+1=8193 此时要向上对齐,按照1024去对齐 8193+1023=9216 1023/9216=0.1 10% 浪费了10%左右
// [64*1024+1,256*1024] 8*1024byte对齐 freelist[184,208)
//弄成静态的不需要成员变量去调用
//把要申请的内存按照桶的要求去进行对齐,得到对齐(按照每个桶对应的内存块大小)后的内存块大小
static inline size_t _RoundUp(size_t bytes, size_t alignNum)
{
size_t alignSize;
if (bytes % alignNum != 0)// 10%8=2 此时需要对齐
{
alignSize = (bytes / alignNum + 1) * alignNum;//此时申请8个bytes
}
else
{
alignSize = bytes;
}
return alignSize;
}
static inline size_t RoundUp(size_t size)
{
if (size <= 128)
{
return _RoundUp(size, 8);
}
else if (size <= 1024)
{
return _RoundUp(size,16);
}
else if (size <= 8 * 1024)
{
return _RoundUp(size, 128);
}
else if (size <= 64 * 1024)
{
return _RoundUp(size, 1024);
}
else if (size <= 256 * 1024)
{
return _RoundUp(size, 8 * 1024);
}
else
{
return _RoundUp(size, 1 << PAGE_SHIFT);//>256k就按照页为单位去进行对齐
}
}
//计算是第几号桶
static inline size_t _Index(size_t bytes,size_t alignNum)
{
if (bytes % alignNum == 0)
{
return bytes / alignNum - 1;
}
else
{
return bytes / alignNum;
}
}
//计算映射的哪一个自由链表桶
static inline size_t Index(size_t bytes)
{
assert(bytes <= MAX_BYTES);
//每个区间有多少个链
static int group_array[5] = { 16, 56, 56, 56, 24 };
if (bytes <= 128)
{
return _Index(bytes, 8);
}
else if (bytes <= 1024)
{
return _Index(bytes - 128, 16) + group_array[0];
}
else if (bytes <= 8 * 1024)
{
return _Index(bytes - 1024, 128) + group_array[1] + group_array[0];
}
else if (bytes <= 64 * 1024)
{
return _Index(bytes - 8192, 1024) + group_array[2] + group_array[1] + group_array[0];
}
else if(bytes <= 256 * 1024)
{
return _Index(bytes - 65536, 8 * 1024) + group_array[3] + group_array[2] + group_array[1] + group_array[0];
}
else
{
assert(false);
}
return -1;
}
//一次thread cache从中心缓存(central cache)获取多少个
static size_t NumMoveSize(size_t size)//size 对齐后的size(alignSize)
{
//if (size == 0)
// return 0;
assert(size > 0);
//[2byte,512byte] 一次批量移动多少个对象的(慢启动)上限值
int num = MAX_BYTES / size;
//小对象给大块,大对象给小块
//控制centralcache给threadcache的内存块大小的上下限
if (num < 2)
num = 2;
if (num > 512)
num = 512;
return num;
}
//计算一次向系统获取几个页
//单个对象 8byte
//......
//单个对象 256byte
//
//就是你这个centralcache对应的桶对齐到pagecache的桶中
//根据CentralCache中那个桶里需要申请内存,根据这个桶去计算出
//在PageCache对应的是哪个桶
static size_t NumMovePage(size_t size)
{
size_t num = NumMoveSize(size);
size_t npage = num * size;
npage >>= PAGE_SHIFT;
if(npage == 0)
npage = 1;
return npage;
}
};
//Span管理一个跨度的大块内存(用于centralcache当中)
//管理以页为单位的大块内存
struct Span
{
//页号 用2进制位来表示所有的页号 32位程序 2^32/2^13=2^19个页 size_t对
//应4个字节 4个字节可以表示32个比特位(1个字节8个比特位) 可以表示的状态有2^32种状态 2^32
//种状态可以表示2^19个页
//64位程序 2^64/2^13(一页的大小 设:一页等于8KB)=2^51个页 一个size_t=4个字节 可以表示2^32
//种状态 2^32表示不完2^51
PAGE_ID _pageId = 0;
size_t _n = 0; //页的数量
Span* _next = nullptr; //双向链表的结构
Span* _prev = nullptr;
size_t _objSize = 0; //切好的小对象的大小
size_t _useCount = 0;//切好小块内存,被分配给thread cache的计数 还回来一个--,给出去一个
//++
void* _freeList = nullptr;//指向切好的小块内存构成的自由链表的头节点
bool _isUse = false; //是否在被使用
};
//带头双向循环链表(centralcache中每个桶后面都有由span形成的双向带头循环链表)
class SpanList
{
public:
SpanList()
{
_head = new Span;
_head->_next = _head;
_head->_prev = _head;
}
Span* Begin()
{
return _head->_next;//哨兵位的下一个节点
}
Span* End()
{
return _head;//哨兵位的头
}
bool Empty()
{
return _head->_next == _head;
}
void PushFront(Span* span)
{
Insert(Begin(), span);//以头插的形式把这个span插入到这个SpanList当中
}
Span* PopFront()
{
Span* front = _head->_next;//这是一个带头双向循环链表
Erase(front);
return front;
}
void Insert(Span* pos,Span* newSpan)
{
assert(pos);
assert(newSpan);
Span* prev = pos->_prev;
//prev newSpan pos
prev->_next = newSpan;
newSpan->_prev = prev;
pos->_prev = newSpan;
newSpan->_next = pos;
}
void Erase(Span* pos)
{
assert(pos);
assert(pos != _head);
Span* prev = pos->_prev;
Span* next = pos->_next;
prev->_next = next;
next->_prev = prev;
}
private:
Span* _head;
public:
std::mutex _mtx;//桶锁
};
解释下面这个规则:
3.2.2 ThreadCache部分
声明 ThreadCache.h
#pragma once
#include"Common.h"
class ThreadCache
{
public:
//申请和释放内存对象
void* Allocate(size_t size);
void Deallocate(void* ptr, size_t size);
//从中心缓存获取对象
void* FetchFromCentralCache(size_t index, size_t size);//threadcache中哪个桶需要申请内存块,这个桶又需要申请多少(我根据申请的大小向centralcache去申请内存块 对齐式的申请 多申请的内存留下以后用)
//释放对象时,链表过长时,回收内存回到中心缓存
void ListTooLong(FreeList& list, size_t size);
private:
FreeList _freeLists[NFREELIST];//保存 自由链表类型的变量 的数组 变量数组
};
//用静态TLS方法存储的变量,这个变量在它所在的线程内是全局访问的,但是不能被其他线程访问,因而实现
//了变量的线程独立性。加上static保证唯一性,当被多个文件同时包含时
static _declspec(thread) ThreadCache* pTLSThreadCache = nullptr;
定义 ThreadCache.cpp
#include"ThreadCache.h"
#include"CentralCache.h"
//编写的代码:高内聚,低耦合
//FetchFromCentralCache是
//让threadcache向centralcache申请内存的接口
//此时threadcache向centralcache申请内存的时候不是,你要一个字节我就拿一个字节给你
//而是你要一个字节我就预先给你一部分,这一部分剩下的以后也可以用——>可以防止重复的
//向centralcache申请造成桶的拥堵,一种预先申请的思想
void* ThreadCache::FetchFromCentralCache(size_t index, size_t size)
{
//慢开始反馈调节算法(这一部分的大小是逐渐的由小到大递增的,一开始给一小部分,然后这一部分越
//给越大)
//慢开始反馈调节算法
//1.最开始不会一次向centralcache一次批量要太多,因为要太多了可能用不完
//2.如果你不要这个size大小内存需求,那么batchNum就会不断增长,直到上限
//3.size越大,一次向centralcache要的batchNum就越小
//4.size越小,一次向centralcache要的batchNum就越大(这个的越大是慢慢的增长的)
size_t batchNum = min(_freeLists[index].MaxSize(), SizeClass::NumMoveSize(size));
//你的size越小,得到的NumMoveSize(size)就越大,此时我选择给小的MaxSize(),实际我不是一下子
//给一大块,而是慢增长的去给
if (_freeLists[index].MaxSize() == batchNum)
{
_freeLists[index].MaxSize() += 1;//实现了一个慢的增长
}
void* start = nullptr;//void* start 是一个局部的变量
void* end = nullptr;
size_t actualNum = CentralCache::GetInstance()->FetchRangeObj(start, end, batchNum, size);//调用FetchRangeObj接口向CentralCache申请内存
assert(actualNum > 0);
if (actualNum == 1)//actualNum实际申请的数量为1个时,从CentralCache桶当中的span下面
{
assert(start == end);
NextObj(start) = nullptr;
return start;
}
else//当申请的actualNum的数量是大于1的
{
//把申请的内存挂到桶上
_freeLists[index].PushRange(NextObj(start),end,actualNum-1);//一段内存是由链表构成
//的,返回头节点的地址start,返回尾节点的地址end
return start;
}
}
//通过Allocate去向threadcache中对应的桶当中申请内存块然后在把这个内存块的地址返回出去(malloc)
void* ThreadCache::Allocate(size_t size)//size_t 无符号整形 1 int = 4 字节
{
assert(size <= MAX_BYTES);
size_t alignSize = SizeClass::RoundUp(size);//计算得到了对齐后的size,向桶对齐
size_t index = SizeClass::Index(size);//计算你对应的是哪个桶
//对threadcache数组其中的某一个桶去进行申请内存块
//你要申请的size已经在桶当中去进行了对应(利用RoundUp),此时你到这个对应的桶当中申请时,内
//存块的大小一定是可以满足你的需求的,不用考虑 申请的内存大小< size(实际需要的大小) 的情
//况
if (!_freeLists[index].Empty())//ThreadCache中的index桶不为空,申请内存块
{
return _freeLists[index].Pop();//在threadcache某个桶当中 头删 式的去取一个内存块,然
//后返回这个内存块的起始地址
}
else//threadcache当中的这个桶没有内存块
{
void* start = FetchFromCentralCache(index, alignSize);
if (NextObj(start) == nullptr)
{
return start;
}
else//当申请的actualNum是大于1的,把申请的内存块挂到ThreadCache的桶当中
{
return Allocate(size);//然后在回调Allocate函数去头删,随后在返回弹出的内存块地址
}
}
}
//释放内存对象--->就是把释放的内存块重新串接到对应的桶的链表当中
void ThreadCache::Deallocate(void* ptr, size_t size)
{
assert(ptr);
assert(size <= MAX_BYTES);
//找到映射的自由链表桶,对象插入进入
size_t index = SizeClass::Index(size);//算出对应的桶号
_freeLists[index].Push(ptr);//把不用的内存块直接放入ThreadCache对应的桶中
//当链表长度大于一次批量申请的内存时就开始还一段list给central cache
if (_freeLists[index].Size() >= _freeLists[index].MaxSize())
{
ListTooLong(_freeLists[index], size);//这一段内存通过调用ListTooLong还给
//CentralCache
}
}
void ThreadCache::ListTooLong(FreeList& list, size_t size)
{
void* start = nullptr;
void* end = nullptr;
//从threadcache当中把内存回收
list.PopRange(start, end, list.MaxSize());//在对应的桶中根据list.MaxSize()去取一段
CentralCache::GetInstance()->ReleaseListToSpans(start, size);//调用ReleaseListToSpans
//还给CentralCache
}
我们在实际的调用thread cache当中的接口的时候我们还进行了一层封装,这层封装的作用是保证每个线程申请的时候都有自己独自的thread cahe
ConcurrentAlloc.h
#pragma once
#include"Common.h"
#include"ThreadCache.h"
#include"PageCache.h"
#include"ObjectPool.h"
//加上static使全局的变为静态的,使它只在当前文件当中可见,假如不设置为静态的
//当这个文件被重复的包含时,当预编译把头文件展开时,会在包含这个头文件的每一
//个文件当中都有一个这个函数,此时就会存在链接属性发生冲突的问题
//
//利用ConcurrentAlloc去封装一下Allocate,这样每次在线程创建threadcache前先保证自己这个线程申请的threadcache只有自己可以看见
static void* ConcurrentAlloc(size_t size)//申请了一个内存块要返回这个内存块的起始地址 void*(malloc)
{
if (size > MAX_BYTES)//当申请的内存是大于256*1024的
{
size_t alignSize = SizeClass::RoundUp(size);//把需要申请的size向上对齐
size_t kpage = alignSize >> PAGE_SHIFT;//把对齐后的size通过移位算出页数
PageCache::GetInstance()->_pageMtx.lock();
Span* span = PageCache::GetInstance()->NewSpan(kpage);//调用PageCache中的NewSpan去申请内存
span->_objSize = size;//size 是你要申请的内存块大小
PageCache::GetInstance()->_pageMtx.unlock();
void* ptr = (void*)(span->_pageId << PAGE_SHIFT);//得到初始地址
return ptr;
}
else//当申请的内存是小于256*1024的
{
//通过TLS 每个线程无锁的获取自己的专属的ThreadCache对象
if (pTLSThreadCache == nullptr)
{
static ObjectPool<ThreadCache> tcPool;
pTLSThreadCache = tcPool.New();//调用tcPool.New()得到一个ThreadCache对象
}
return pTLSThreadCache->Allocate(size);//调用ThreadCache.cpp中的Allocate去申请内存
//块
}
}
static void ConcurrentFree(void* ptr)
{
Span* span = PageCache::GetInstance()->MapObjectToSpan(ptr);
size_t size = span->_objSize;
if (size > MAX_BYTES)
{
PageCache::GetInstance()->_pageMtx.lock();
PageCache::GetInstance()->ReleaseSpanToPageCache(span);
PageCache::GetInstance()->_pageMtx.unlock();
}
else
{
assert(pTLSThreadCache);
pTLSThreadCache->Deallocate(ptr, size);
}
}
3.2.3 CentralCache部分
声明 CentralCache.h
#pragma once
#include"Common.h"
//CentralCache是全局唯一的 饿汉模式 桶锁,因为不同的桶之间是不会存在竞争的,而相同的桶之间才会存在竞争
//单例模式
class CentralCache
{
public:
static CentralCache* GetInstance()//不加上static就只能通过变量去调用这个接口
{
return &_sInst;
}
//获取一个非空的span
Span* GetOneSpan(SpanList& list,size_t byte_size);
//从中心缓存获取一定数量的对象给threadcache
size_t FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size);
//将一定数量的对象释放到span跨度
void ReleaseListToSpans(void*& start, size_t byte_size);
private:
SpanList _spanLists[NFREELIST];//把一个打页的span切为若干个小的内存块用单向链表管理
private:
CentralCache()
{}
CentralCache(const CentralCache&) = delete;
CentralCache& operator=(const CentralCache&) = delete;
//定义一个自己类型的对象(相当于全局的,但是此时受类域的限制)
static CentralCache _sInst;//_sInst在类里声明 变量在创建对象的时候实例化
};
定义 CentralCache.cpp
#include"CentralCache.h"
#include"PageCache.h"
CentralCache CentralCache::_sInst;//_sInst在类外定义 _sInst是一个静态的,在代码执行之前初始化,属于 编译期 初始化,保证全局的唯一性
//获取一个非空的span
Span* CentralCache::GetOneSpan(SpanList& list, size_t size)
{
Span* it = list.Begin();
while (it != list.End())
{
if (it->_freeList != nullptr)
{
return it;
}
else
{
it = it->_next;
}
}
//先把central cache的桶锁解掉,这样如果有其他的线程释放内存对象回来,也不会阻塞
list._mtx.unlock();
//走到这里说明没有空闲的span了,只能找page cache要 size是你要申请的大小,但是你向我申请
//size,而会直接按照pagecache的规则给你一部分
PageCache::GetInstance()->_pageMtx.lock();
//调用NewSpan向PageCache去获取一个span
Span* span = PageCache::GetInstance()->NewSpan(SizeClass::NumMovePage(size));
//向PageCache申请内存,向PageCache申请内存也是存在线程安全问题的
span->_isUse = true;//标记这个span是已经被使用了的
span->_objSize = size;//设置这个span的大小
PageCache::GetInstance()->_pageMtx.unlock();
//对获取的span进行切分,不需要加锁,因为这会其他线程访问不到这个span,因为你访问pagecache的
//时候是加了锁的
//把从pagecache获取到的span我进行切分,然后链接到CentralCache当中
//计算span的大块内存的起始地址和大块内存的大小(字节数)
//就是我pagecache一开始是为空的,然后申请直接申请一个128页的内存,此时我128页的内存经过不断
//切分,当切分到了对应的桶当中时,我取走一个span,我要计算我取的这个span在整个128页的span中
//是哪个位置,这样方便后面的合并然后归还给堆
char* start = (char*)(span->_pageId << PAGE_SHIFT);
//通过一个页的页号就可以得到它的地址 如:从0页开始 你现在是100页 100<<13=819200 =
//100*8*1024=819200Bytes (此时就知道了你这次申请的这块内存块的起始地址是第819200Byte
//(address)处 从0Byte开始)
size_t bytes = span->_n << PAGE_SHIFT;//此时得到了这个span的大小
char* end = start + bytes;//计算在整体当中此时申请的这个span的结尾位置
//把大块内存切成自由链表链接起来
//1.先切一块下来去做头,方便尾插(这个尾插的好处是逻辑上是离散的,但是在物理上还是连续的,用
//头插的话就打乱了连续性)
span->_freeList = start;
start += size;//在大块的span中,指向下一个要切分的位置
//size就是前几层传来的alignSize,就是与桶对齐后的size,也就是这个桶后面要链接的每一个内存块
//size就是对应这个桶中要切为的大小,就是按照size去切。centralcache和threadcache桶对应的规
//则是一致的。
void* tail = span->_freeList;
while (start < end)
{
NextObj(tail) = start;//把下一个要切分的内存块链接到第一个内存块的后面
tail = NextObj(tail);//更新tail的位置
start += size;//start指向下一个要切的位置
}
NextObj(tail) = nullptr;//切到最后把最后一个节点指向的位置置为nullptr
//span是一个结构体,这个结构体管理着它下面切分好的小内存块
//切好span当中的指针指向的内存块以后,需要把span挂到桶里面去的时候,再加锁
list._mtx.lock();
list.PushFront(span);//这个SpanList当中去插入一个span,而这个span下面已经链接好了切好的内
//存块
return span;
}
//从中心缓存获取一定数量的对象给thread cache
size_t CentralCache::FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size)
{
size_t index = SizeClass::Index(size);//计算在CentralCache中申请对应的桶号
//桶锁
_spanLists[index]._mtx.lock();
//在centralcache中找到一个span,这个span下面有内存块
Span* span = GetOneSpan(_spanLists[index],size);//获取一个span对象
assert(span);
assert(span->_freeList);//span->_freeList 为空表示这个span下面为空
//当找到了这样的一个下面有内存块的span后,到这个span下面挂的内存块中去取自己需要的内存块数量
//从span中获取batchNum个对象
//如果不够batchNum个,有多少拿多少
start = span->_freeList;
end = start;
size_t i = 0;
size_t actualNum = 1;//你实际获取到的span下内存块的个数
while(i < batchNum - 1 && NextObj(end) != nullptr)//可能出现一个span下有4个内存块,但是实际你要申请5个
{
end = NextObj(end);
++i;
++actualNum;
}
span->_freeList = NextObj(end);//重置这个CentralCache当中对应的桶后面挂的span中
//_freeList指向(这个桶是这次申请时要访问的桶)
NextObj(end) = nullptr;//把要取的这段链式结构的内存块中末尾内存块最后的指向置为nullptr
//_useCount管理span下链接的内存块数量
span->_useCount += actualNum;//_useCount:拿走一个就++,还来一个就--
_spanLists[index]._mtx.unlock();
return actualNum;
}
void CentralCache::ReleaseListToSpans(void*& start, size_t size)
{
size_t index = SizeClass::Index(size);//根据size去求在CentralCache下对应的桶号
_spanLists[index]._mtx.lock();//归还内存要加桶锁
while (start)
{
void* next = NextObj(start);//保存start的下一个内存块
Span* span = PageCache::GetInstance()->MapObjectToSpan(start);//在map中查找span和
//地址对应的映射规则
NextObj(start) = span->_freeList;//取ThreadCache还来的一段节点中的第一个节点链入
//CentralCache对应的span下的链表当中,以头插的形式
span->_freeList = start;//更新span下的_freeList
span->_useCount--;//当还来一个_useCount就--
//当_useCount=0了,说明span的切分出去的所有小块内存都回来了
//这个span就可以再回收给page cache,page cache可以尝试去做前后页的合并
if (span->_useCount == 0)
{
_spanLists[index].Erase(span);
span->_freeList = nullptr;
span->_next = nullptr;
span->_prev = nullptr;
//释放span给page cache时,释放访问这个span对应的那个桶锁
_spanLists[index]._mtx.unlock();
//把满足条件的span还给PageCache
PageCache::GetInstance()->_pageMtx.lock();
PageCache::GetInstance()->ReleaseSpanToPageCache(span);
PageCache::GetInstance()->_pageMtx.unlock();
_spanLists[index]._mtx.lock();
}
start = next;
}
_spanLists[index]._mtx.unlock();
}
3.2.4 PageCache部分
声明 PageCache.h
#pragma once
#include"Common.h"
#include"ObjectPool.h"
class PageCache
{
public:
static PageCache* GetInstance()
{
return &_sInst;
}
//获取从对象到span的映射
Span* MapObjectToSpan(void* obj);
//释放空闲span回到PageCache,并合并相邻的span
void ReleaseSpanToPageCache(Span* span);
//获取一个k页的span
Span* NewSpan(size_t k);
std::mutex _pageMtx;
private:
SpanList _spanLists[NPAGES];
ObjectPool<Span> _spanPool;
std::unordered_map<PAGE_ID, Span*> _idSpanMap;//建立span和页号的对应关系
std::unordered_map<PAGE_ID, size_t> _idSizeMap;
PageCache()
{}
PageCache(const PageCache&) = delete;
PageCache& operator=(const PageCache&) = delete;
static PageCache _sInst;
};
定义 PageCache.cpp
#include"PageCache.h"
PageCache PageCache::_sInst;
//获取一个k页的span
Span* PageCache::NewSpan(size_t k)
{
assert(k>0 && k< NPAGES);//PageCache中只有1到129号桶这128个桶
//大于128 page的直接向堆申请
if (k > NPAGES - 1)
{
void* ptr = SystemAlloc(k);//SystemAlloc是直接向系统申请的接口
//Span* span = new Span;
Span* span = _spanPool.New();//申请一个span对象
//用这个span对象管理从系统申请的大块内存
span->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;
span->_n = k;
//把这个span放入Map去保存起来
_idSpanMap[span->_pageId] = span;
return span;
}
//先检查第k个桶里面有没有span,有span就申请
if (!_spanLists[k].Empty())
{
Span* kSpan = _spanLists[k].PopFront();//PageCache对应的桶弹出一个内存块
//把这个拿到的内存块按页去在Map中建立映射关系
for (PAGE_ID i = 0; i < kSpan->_n; ++i)
{
_idSpanMap[kSpan->_pageId + i] = kSpan;
}
return kSpan;
}
//检查一下后面的桶里面有没有span,如果有可以把他进行切分,
//如:要一个1页的span,可是我1page对应的桶当中没有,我就向后面的桶当中找
//假如3page的桶当中有内存块,把3page切为1page+2page,1page拿给你用,
//2page的在挂到对应桶上
for (size_t i = k + 1; i < NPAGES; ++i)//从这个要申请的却没有内存块的桶向下找其他的桶
{
if (!_spanLists[i].Empty())//此时找到了这个桶
{
Span* nSpan = _spanLists[i].PopFront();//头部弹出一个
//Span* nSpan是一个结构体,这个结构体下面保存了一块页(内存块)
Span* kSpan = new Span;//创建一个Span对象去管理要向上交付的内存块
//在nSpan的头部切一个k页下来(k页是你实际想申请的大小)
//知道这个页的起始地址,知道你有几页,就可以算出你这个内存块的结束地址
//设置kSpan当中的信息,信息是你要申请的内存块
kSpan->_pageId = nSpan->_pageId;
//_pageId是你申请的这个span的起始页号
//_pageId: 在128页的大块内存中你是第几页的,因为一次是申请128页的大块内存
kSpan->_n = k;//就是你需要多少的页
//我知道你这个页的起始地址,我还知道你有几页就可以知道这个内存块的结束地址
//重置从PageCache对应桶中弹出的span中对应的信息
nSpan->_pageId += k;
nSpan->_n -= k;
//把不需要的内存块挂到对应的PageCache的桶中
_spanLists[nSpan->_n].PushFront(nSpan);
//存储nSpan的首位页号跟nSpan映射,方便page cache回收内存时进行的合并查找
_idSpanMap[nSpan->_pageId] = nSpan;
_idSpanMap[nSpan->_pageId + nSpan->_n - 1] = nSpan;
//建立id和span的映射,方便central cache回收小块内存时,查找对应的span
for (PAGE_ID i = 0; i < kSpan->_n; ++i)
{
_idSpanMap[kSpan->_pageId + i] = kSpan;
}
//返回需要申请的内存块
return kSpan;
}
}
//走到这个位置就说明后面没有大页的span了,此时向堆去进行申请一个128页的span并且拿一个Span的
//结构体管理起来
Span* bigSpan = new Span;//创建管理的span
void* ptr = SystemAlloc(NPAGES - 1);//向系统申请一个128page的span
bigSpan->_pageId = (PAGE_ID)ptr >> PAGE_SHIFT;//把整个堆空间看成是由一个个的页构成的,我
//从这整个空间中拿了128页,那这个第1号页就是这个128页的起始页号,而这个第1页在整个堆当中是位
//于第多少页呢(此时对应的就是_pageId,而_pageId是用整形保存的)
bigSpan->_n = NPAGES - 1;//保存的页的数量
_spanLists[bigSpan->_n].PushFront(bigSpan);//把向堆申请的span链接到PageCache的桶当中
return NewSpan(k);
}
Span* PageCache::MapObjectToSpan(void* obj)
{
PAGE_ID id = ((PAGE_ID)obj >> PAGE_SHIFT);
std::unique_lock<std::mutex> lock(_pageMtx);//读取和写入map怕同时进行,因为map的底层是红
//黑树
auto ret = _idSpanMap.find(id);//根据页号查找对应的span,当find查找不到时,返回
//_idSpanMap.end()
if (ret != _idSpanMap.end())//判断这个ret的正确性,end()该迭代器位于map中的最后一个条目旁
//边
{
return ret->second;
}
else
{
assert(false);
return nullptr;
}
}
void PageCache::ReleaseSpanToPageCache(Span* span)
{
//大于128 page的直接还给堆
if (span->_n > NPAGES - 1)
{
void* ptr = (void*)(span->_pageId << PAGE_SHIFT);//把页号转化为对应地址
SystemFree(ptr);//调用SystemFre中封装的VirtualFree去释放空间
//delete span;
_spanPool.Delete(span);//释放span对象
return;
}
//小于128 page去进行合并
//对span前后的页,尝试进行合并,缓解内存碎片问题
//把还来的span和前面的页去合并
while (1)
{
PAGE_ID prevId = span->_pageId - 1;//查找这个span前一个ID(页号)
auto ret = _idSpanMap.find(prevId);//因为在map中建立了每一个span和_pageId对应的映射
//关系,在map中去根据前一个页号查找对应的span
//前面的页号没有,不合并了
if (ret != _idSpanMap.end())
{
break;
}
//前面相邻页的span在使用,不合并了
Span* prevSpan = ret->second;
if (prevSpan->_isUse == true)
{
break;
}
// 合并出超过128页的span没办法管理(因为在堆上申请的2个128页的内存块可能是紧紧的挨着
// 的),不合并了
if (prevSpan->_n + span->_n > NPAGES - 1)
{
break;
}
//此时符合条件去进行向前合并
span->_pageId = prevSpan->_pageId;//更新_pageId的位置
span->_n += prevSpan->_n;//_n表示页的数量
_spanLists[prevSpan->_n].Erase(prevSpan);//把合并了的页从桶中删除掉
//delete prevSpan;
_spanPool.Delete(prevSpan);//把创建的prevSpan释放掉,目的是清空里面保存的内容
}
//向后合并
while (1)
{
PAGE_ID nextId = span->_pageId + span->_n;
auto ret = _idSpanMap.find(nextId);
if (ret == _idSpanMap.end())
{
break;
}
Span* nextSpan = ret->second;
if (nextSpan->_isUse == true)
{
break;
}
if (nextSpan->_n + span->_n > NPAGES - 1)
{
break;
}
span->_n += nextSpan->_n;
_spanLists[nextSpan->_n].Erase(nextSpan);
//delete nextSpan;
_spanPool.Delete(nextSpan);
}
_spanLists[span->_n].PushFront(span);
span->_isUse = false;
_idSpanMap[span->_pageId] = span;
_idSpanMap[span->_pageId + span->_n - 1] = span;
//内存池从堆当中申请的页是不会还回去给堆的
}
3.2.5 优化(替换掉new)
#pragma once
#include"Common.h"
//template<size_t size>
//class ObjectPool
//{
//
//};
//ObjectPool 替代 new 的
template<class T>
class ObjectPool
{
public:
~ObjectPool()
{
// ...
}
void*& NextObj(void* obj)
{
return *((void**)obj);
}
T* New()
{
T* obj = nullptr;
if (_freeList)
{
obj = (T*)_freeList;
//_freeList = *((void**)_freeList);
_freeList = NextObj(_freeList);
}
else
{
if (_leftSize < sizeof(T))
{
_leftSize = 1024 * 128;
//_memory = (char*)malloc(_leftSize);
_memory = (char*)SystemAlloc(_leftSize >> 13);
if (_memory == nullptr)
{
//exit(-1);
//cout << "malloc fail" << endl;
throw std::bad_alloc();
}
}
obj = (T*)_memory;
size_t objSize = sizeof(T) < sizeof(void*) ? sizeof(void*) : sizeof(T);
_memory += objSize;
_leftSize -= objSize;
}
//定位new,显示调用T的构造函数初始化
new(obj)T;
return obj;
}
void Delete(T* obj)
{
obj->~T();
// 头查到freeList
//*((int*)obj) = (int)_freeList;
//*((void**)obj) = _freeList;
NextObj(obj) = _freeList;
_freeList = obj;
}
private:
char* _memory = nullptr;
int _leftSize = 0;
void* _freeList = nullptr;
};
补充(大概了解):
什么是线程池?
因为频繁的开启线程或者停止线程,线程需要重新从cpu从就绪状态调度到运行状态,需要发送cpu的上下文切换,效率非常低。
因此我们可以提前申请好一批线程,这些线程是用一个池子去管理起来的,让这批线程一直保持运行状态,当某项任务需要交给某个线程去执行时,直接把这个任务扔给这些提前申请好的且一直在保持运行的线程让它们去完成这个任务,就是一种复用,预先申请的机制。