由于本人发现,项目每推进一步的时候,往往还要对之前已经完成的部分进行修改 ,因此以后本栏目文章将不会再向02节那样详细得讲每一个功能模块了,只记录关键之处(项目的完整框架及代码,后续会陆续上传到本人的Gitee仓库中)。
中心资源层的逻辑结构和线程资源层几乎是一样的,只不过是多个线程共享使用中心资源层。
一、Span块
中心资源层也是采用桶链来存放内存块的,但是比线程资源层的桶链多了一层,线程资源层位桶➡内存块结构,而中心资源层为桶➡内存包(Span)➡内存块的结构,如图所示:
//内存包span,里面包含了连续页的内存,是中心资源层和页缓存资源层的重要结构
struct Span
{
PAGE_ID pageID ;//包中起始页在用户内存地址空间中的页编号!
size_t counts ;//包中页的数量
Span* prev ;
Span* next ;
void* freelist ;//老成员,桶类,里面存放着span内存包中被切好的内存块
size_t usedBlocks ;//已经被分配出去的内存块数量
};
(pageID在页缓存资源层极为重要,它实际上才在页缓存资源层标识了span包中内存块的起始地址,而freelist在底层实际上是空置的,只有其上交给了中心资源层,才会根据pageID计算出第一个内存块的地址)
而SpanList桶实际就是一个Span包组成的链表了:
class SpanList
{
public:
SpanList()
{
head = new Span;
head->prev = head;
head->next = head;
}
void insert(Span*pos,Span*newNode)
{
newNode->next = pos;
newNode->prev = pos->prev;
pos->prev = newNode;
newNode->prev->next = newNode;
}
void erase(Span* pos)
{
assert(pos!=head);
pos->prev->next = pos->next;
pos->next->prev = pos->prev;
}
mutex& Getmutex()
{
return mt;
}
private:
Span* head;
mutex mt;//中心资源层,每个SpanList链都必须加锁,以防止多个线程一下申请多个同样大小的内存块时(如果是不同大小的内存块,那么就会向不同的中心桶索取,不存在锁竞争的问题)。
};
(注意,开头的图里为了阐明桶的包含关系,没有突出底层的数据结构,这个SpanList是一个双向带头循环链表)。
二、单例模式
由于是多个线程都会使用同一个中心资源层(CentreCache),因此,根据程序得规范,最好将中心资源层设计成单例模式,以使得整个程序中最多只出现一个中心资源层对象。
饿汉模式:
class CentreCache
{
public:
static CentreCache& GetCentreCache()
{
return self;
}
size_t GetRangeMemory(void*&start, void*&end, size_t n, size_t index);//start、end为输出型参数,返回span内存包中分配出去的部分内存块链的起始和尾部,n代表要分配的内存块数量,index代表spanLists链的桶编号(与ThreadCache层的桶的编号是对应的),返回值为实际分配出去的内存块数量
Span* GetOneSpan(SpanList* sl, size_t size);
private:
CentreCache() {};
CentreCache(const CentreCache&) = delete;
CentreCache& operator=(const CentreCache&) = delete;
private:
SpanList spanLists[BUCKETSIZE];
static CentreCache self;
};
CentreCache CentreCache::self;//必须放在.cpp文件中,如放在.h头文件中会导致重复定义的问题
size_t CentreCache::GetRangeMemory(void*& start, void*& end, size_t n, size_t index)
{
spanLists[index].Getmutex().lock();//加上桶锁
Span* sp = GetOneSpan(&spanLists[index], index,alignedSize);
assert(sp&&sp->freelist!=nullptr);
size_t num = n-1;//end要下走的步数
start = sp->freelist;
end = start;
while (num > 0 && NextNode(end) != nullptr)
{
--num;
end = NextNode(end);
}
sp->freelist = NextNode(end);
sp->usedBlocks += n - num;//n-num即为实际分配出去的内存块数量
spanLists[index].Getmutex().unlock();
return n - num;//n-num即为actualNumber
}
Span* CentreCache::GetOneSpan(SpanList* sl, size_t index, size_t alignedSize)//从中心资源层找到一个符合线程资源层所申请的Span包,如没有,向页缓存资源层申请一个新的Span块
{
//后续实现
return nullptr;
}
注意,中心资源层的每个桶可能同时被多个线程的ThreadCache访问,因此必须加锁,由于锁是加在每个桶里的,因此被称为桶锁,当多个线程虽然同时访问中心资源层,但若访问的不是同一个桶,那么对效率的影响微乎其微。
三、02节中还未完成的RequestFromCentralCache函数
RequestFromCentralCache函数属于线程资源层,作用是向中心资源层申请空间,当中心资源层内存块也不足的时候,会调用中心资源层的 GetOneSpan函数。
RequestFromCentralCache函数被调用后,在其内部首先应对请求的内存块进行分析,判断一次向中心资源层申请多少内存块好(如果请求多少,就向下申请多少,效率就太低了,应当多申请一些然后将多余的内存块存进线程资源层对应的freelists链表中;但也不宜申请太多,以免浪费)。而项目采用慢开始算法来解决这个问题,随着某个线程请求同一大小的内存块的次数的增多, RequestFromCentralCache函数一次向下申请的内存块数也越来越多,直至达到对应内存块大小设定的硬上限数量。
void* ThreadCache::RequestFromCentralCache(size_t index, size_t alignedSize)
{
size_t ReqBlockNum = min(freelists[index].MaxSize, SizeClass::blockCountCal(alignedSize));//慢开始算法,blockCountCal决定每种内存块一次申请的上限
if (ReqBlockNum == freelists[index].MaxSize)//如果实际申请内存块数等于MaxSize(未达上限),那么MaxSize线性增大,下次再申请同一大小的内存块时,将申请更多
{
freelists[index].MaxSize += 2;
}
void* start = nullptr;
void* end = nullptr;
CentreCache& cenCache = CentreCache::GetCentreCache();
size_t actualNumber = cenCache.GetRangeMemory(start, end, ReqBlockNum, index);
assert(actualNumber > 0);
if (actualNumber >1)
{
freelists[index].PushRangeBlock(NextNode(start), end);//如果申请到的内存块数大于1,就将多余的内存块放进桶里去
}
return start;
}
//其中freelist类的MaxSize和PushRangeBlock都是在前一章基础上额外增添的内容,这里不作复现,详情见完整项目代码。