目录
CentralCache的实现
主体框架
Span
页与页号
WIN32、_WIN32、_W64的区别
条件编译
SpanList
为ThreadCache分配内存结点
补充内容1
补充内容2
具体实现
从PageCache申请非空span
补充内容
具体实现
PageCache的实现
主体框架
关于整体加锁的解释
桶锁机制下的操作流程
Span的分裂
Span的合并
获取新span
补充内容1
补充内容2
具体实现
加解锁
CentralCache的实现
基本概念:CentralCache也是一个哈希桶结构,其内存与桶的映射关系与Thread Cache相同,不同的是它的每个哈希桶下挂的是双向循环链表SpanList,且每个桶都会加锁,即桶锁
使用双向循环链表SpanList的解释:便于插入和删除span,因为要CentralCache既要分配和回收ThreadCache的内存,又要向PageCache申请内存,链表中的span会有频繁的插入和删除
桶锁机制的解释:每个线程向自己的threadcache申请空间是自由的,不需要加锁,但是当两个或多个线程向自己的threadcache中的相同位置的桶申请内存失败时,就会并发的向centralcache中相应位置的桶申请空间,此时就需要加锁,避免线程安全问题(t1线程向其threadcache的2号桶申请空间时该桶为空,此时若t2线程也向其threadcache的2号桶申请空间且该桶也为空,那么二者就会并发向CentralCache的2号桶申请空间,此时就要加锁,谁先拿到锁谁就先获取到空间)
主体框架
注意事项:为保证一个进程在程序执行过程中只有一个CentralCache类,我们要采用饿汉模式
实现方式:
1、构造和拷贝构造函数私有化
2、创建静态成员变量并提供公有的调用函数
class CentralCache
{
public:
//获取实例化好的静态成员对象的地址
static CentralCache* GetInstance()
{
return &_sInst;
}
//为ThreadCache分配一定数量的来自某个span下自由链表中的内存块
size_t FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size);
//从PageCache获取一个非空的span
Span* GetOneSpan(SpanList& list, size_t size);
private:
SpanList _spanLists[NFREELIST];//centralcache的桶数量与threadcache一样
//单例模式的实现方式是构造函数和拷贝构造函数私有化
CentralCache()
{};
CentralCache(const CentralCache&) = delete;
//C++11中,当我们定义一个类的成员函数时,如果后面使用"=delete"去修饰,那么就表示这个函数被定义为deleted,也就意味着这个成员函数不能再被调用,否则就会出错
//使用饿汉模式,保证程序在启动时就会创建对象,避免了线程安全问题
static CentralCache _sInst;//静态成员变量在编译时就会被分配内存
};
- FetchRangeObj():计算实际可为ThreadCache分配的内存结点个数
- start、end:输出型参数,传入FetchRangeObj前为空,经过该函数后就会指向实际结点
- batchNum:理论上要分配的某个span下的自由链表的结点个数
- size:要申请的内存大小(对齐后的)
- GetOneSpan():从PageCache获取一个非空的span(可能ThreadCache申请的位置上没有CentralCache也没有结点)
注意事项:静态成员变量_sInst的定义不要放在头文件里,否则如果这个头文件被多个源文件包含,那么每个包含该头文件的源文件都会生成 _sInst
的一个定义。这将导致链接器在链接阶段报错,提示符号重复定义
Span
基本概念:span是管理以页为单位的大块内存,一个span中可以有多个页
struct Span
{
size_t _pageId;//连续大块内存页的起始页号
size_t _n;//当前span管理的页的数量
Span* _next = nullptr;//双向链表结构
Span* _prev = nullptr;
size_t _useCount = 0;//切好小块内存,被分配给thread cache的计数
void* _freelist = nullptr; //当前span下挂的自由链表的头指针
};
变量解释与注意事项:
1、span挂在哪个桶下面就会将span总的空间划分成多个对应桶表示的字节大小的空间(span挂在0号桶下面,就会被划分成多个8Byte的内存块)
2、span结构体中会有一个void* _freelist来表示划分好的多个小空间的自由链表的头节点(当threadcache内存不够向CentralCache申请时,就是从span管理的自由链表中拿内存的)
3、每个桶下挂的span所包含的页数是不同的,桶对应代表的字节数越小,页数也就越少,代表字节越大页数也就越大(假设一页为最基本的4KB大小,0号桶代表的是4Byte大小的内存块,那一个span中只用一两个的页就够,因为一页可以被划分为512个4Byte的内存块,两页就有1024个,应该可以满足了;而最后一号桶为256KB,那一个span就要有256 / 4 = 64个页才能凑够一个256KB)
4、_usecount用于记录当前span分配出去了多少个块空间,每分配一块给threadcache,就要++use_count,如果threadcache还回来了一块,那就- -use_count,_usecount初始值为0。当span中的use_count为0的时候可以将其还给pc以供pc拼接更大的页,用来解决外碎片问题
5、span中变量_pageID,用于确定span中所管理的页的页号
页与页号
①OS进行内存管理的基本单位是页,一页的大小通常是4 KB,但也可能更大
②32位环境下进程地址空间是4GB,即2^32 = 4,294,967,296 字节
③64位环境下进程地址空间是2^34GB,即2^64 = 很大的字节
④若规定一页位8KB,那么在32位环境下需要2^32 / 2^13 = 2^19个页,在64位环境下需要2^64 / 2^13 = 2^51个页
WIN32、_WIN32、_W64的区别
①
_WIN32
是一个预定义宏,在所有 Windows 平台(包括 32 位和 64 位)上都会定义。无论是 32 位还是 64 位 Windows 系统,编译器都会定义_WIN32
使用场景:希望在代码中区分 Windows 平台和其他非 Windows 平台时使用
#ifdef _WIN32 // win32或win64环境 #else // 不是win的环境 #endif
②
_WIN64
是一个预定义宏,仅在 64 位 Windows 平台上定义使用场景:希望在代码中区分 64 位 Windows 和 32 位 Windows 时使用
#ifdef _WIN64 // win64环境 #else // win32环境或其它环境 #endif
③
WIN32
是一个历史遗留的宏,在较早的 Windows 编程中常被用来表示 Windows API。然而,它并不总是自动定义的,特别是在现代编译环境中,使用WIN32
宏的地方通常是开发者自己手动定义的,或者是特定库或项目中用来标识使用 Win32 API 的代码段使用场景:需要明确表示代码依赖于 Win32 API 的项目或代码片段中
#ifdef WIN32 // Code that uses Win32 API #endif 在实际项目中,WIN32 可能需要手动在项目设置或代码中定义,如: #define WIN32
条件编译
//仅考虑windows平台
#ifdef _WIN64
typedef unsigned long long PAGE_ID;//64位机器下取用unsigned long long,它的取值范围是2^64
#elif _WIN32
typedef size_t PAGE_ID;//32位机器下取用size_t,它的取值范围是2^32
#endif
- windows32位环境下_WIN64无定义,_WIN32有定义,windows64位环境下,二者均有定义
- 我看了网上有些高并发内存池项目会在这里添加一个运行环境判断的条件编译,即在windows64环境下页号的类型PAGE_ID为unsigned long long,windows32环境下页号的类型为size_t,在32位下选择取值范围为2^32-1的size_t而不是2^64-1的unsigned long long可以理解为不需要那么大的变量来存放页号,但是64位环境下size_t的范围也会变为2^64-1为什么还要再去选择unsigned long long呢?所以我这里的建议是不用加
SpanList
基本概念:管理span的带头双向循环链表
class SpanList
{
public:
//构造初始的span
SpanList()
{
_head = new Span;
_head->_next = _head;
_head->_prev = _head;
}
//在指定位置头插span
//位置描述:prev newspan pos
void Insert(Span* pos, Span* newSpan)
{
assert(pos);//指定位置不能为空
assert(newSpan);//新的span不能为空
Span* prev = pos->_prev;//存放插入位置的前一个span的位置
prev->_next = newSpan;
newSpan->_prev = prev;
newSpan->_next = pos;
pos->_prev = newSpan;
}
//删除指定位置的span
//位置描述:prev pos next
void Erase(Span* pos)
{
assert(pos);//指定位置不能为空
assert(pos != _head);//不能将头指针删除
//暂存一下位置
Span* prev = pos->_prev;
Span* next = pos->_next;
prev->_next = next;
next->_prev = prev;
}
std::mutex _mtx; //桶锁,可以设为公有也可以划为私有并提供return函数
private:
Span* _head = nullptr;
};
为ThreadCache分配内存结点
补充内容1
在FreeList中新增PushRange函数,用于插入除头节点外的其它结点(不是span)
//管理小块内存的自由链表
class FreeList
{
public:
//将多个相连的结点一次性插入
void PushRange(void* start, void* end)//这里的start和CentralCache中提到的start不一样,这里传入的是CentralCache中start指向的下一个结点的地址
{
NextObj(end) = _freeList;
_freeList = start;
}
void Push(void* obj)...
void* Pop()...
bool Empty()...
size_t& MaxSize()...
private:
...
};
补充内容2
在FetchFromCentralCache函数中新增一部分内容
//ThreadCache向CentralCache申请内存块
void* ThreadCache::FetchFromCentralCache(size_t index, size_t size)
{
//size越小上限越高,最高是512
size_t batchNum = min(_freeLists[index].MaxSize(), SizeClass::NumMoveSize(size));
if (_freeLists[index].MaxSize() == batchNum)
{
_freeLists[index].MaxSize() += 1;
}
//上述部分是满调节算法得到的理论上要分配的结点个数
//下面新增的内容是计算实际可拿到的结点个数,以及视情况将申请到的未使用的结点重新挂在ThreadCache的自由链表上
//输出型参数,传入FetchRangeObj函数的是它们的引用,在出该函数后它们就指向某个自由链表中的结点了
void* start = nullptr;
void* end = nullptr;
//理论上要申请的数量batchNum和实际拥有的数量actualNum可能不一致,以实际为主
size_t actualNum = CentralCache::GetInstance()->FetchRangeObj(start, end, batchNum, size);
assert(actualNum >= 1);//因为FetchRangeObj必定会申请到一个自由链表不为空的span,可以参考下面关于FetchRangeObj中的GetOneSpan函数的解释
if (actualNum == 1)
{
assert(start == end);//此时start和end应该都指向该结点
return start;//直接返回start指向的结点即可
}
else
{
_freeLists[index].PushRange(NextObj(start), end);//其余分配的结点插入ThreadCache中的指定位置
return start;//返回一个立刻要使用的start指向的内存结点,
}
}
- actualNum:某个span下的自由链表中实际可获取的最大内存结点的个数
- 满调节算法:高并发内存池(二):整体框架的介绍与ThreadCache的实现
具体实现
注意事项:因为_freeLists是私有的,所以需要指明是CentralCache类下的FetchRangeObj函数才能在该函数中使用_freeLists,否则会出现_freeLists是未声明的标识符
//计算实际可为ThreadCache分配的内存结点个数
size_t CentralCache::FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size)
{
size_t index = SizeClass::Index(size);
_spanLists[index]._mtx.lock();//给指定位置的桶上锁
Span* span = GetOneSpan(_spanLists[index],size);//为index位置的桶下的spanlist申请一个span,传入的是该spanlist的头指针!!!!
assert(span);//申请失败就报错
assert(span->_freelist);//申请成功但自由链表为空也要报错
//尝试从该span下的自由链表中获取bathcNum个内存块,若不够就拿实际拥有的数量actualNum
start = span->_freelist;
end = start;
size_t i = 0;//记录循环遍历结点时经过的结点个数
size_t actualNum = 1;//上面判断过的自由链表不为空,初始值设为1是因为刚开始的时候就已经拿了一个了
//防止实际申请的span中freelist的结点个数小于bathcNum,导致越界访问nullptr产生报错
while (i < batchNum - 1 && NextObj(end) != nullptr)//batchNum-1是因为是数组下标不解释了
{
end = NextObj(end);
++i;
++actualNum;//每获取一个就++
}
span->_freelist = NextObj(end);
NextObj(end) = nullptr;
_spanLists[index]._mtx.unlock();//给指定位置的桶解锁
return actualNum;//返回自由链表中实际可提供的内存块个数
}
补充:在GetOne函数中,会先进行当前桶位置的SpanList上是否有自由链表非空的span,如果有则会提供该span,如果没有才会进一步去找PageCache要,所以该函数保底肯定能申请到一个符合条件的span
从PageCache申请非空span
补充内容
在SpanList类中新增了Begin、End、PushFront三个函数
(Insert函数没有修改只是为了便于观察PushFront函数的功能)
//返回指向链表头结点的指针
Span* Begin()
{
return _head->_next;
}
//返回指向链表尾结点的指针
Span* End()
{
return _head;
}
//头插
void PushFront(Span* span)
{
Insert(Begin(), span);
}
//在指定位置插入span
//位置描述:prev newspan pos
void Insert(Span* pos, Span* newSpan)
{
assert(pos);//指定位置不能为空
assert(newSpan);//新的span不能为空
Span* prev = pos->_prev;//存放插入位置的前一个span的位置
prev->_next = newSpan;
newSpan->_prev = prev;
newSpan->_next = pos;
pos->_prev = newSpan;
}
具体实现
//向PageCache申请一个非空的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;
}
}
//上面是用于遍历指定位置的SpanList,判断该链表中是否还有非空的span,如果有就返回
//当找不到非空的span时就进行下面的内容,即向PageCache申请一个非空的新span
Span* span = PageCache::GetInstance()->NewSpan(SizeClass::NumMovePage(size));
//计算span管理下的大块内存的起始地址和整个span的大小
//起始地址 = 页号 * 页的大小,这里还是用<<计算更快,也可以选择 * 2^13
char* start = (char*)(span->_PageId << PAGE_SHIFT);//选择char*而不是void*便于每次+的时候是一字节
//假设span->_PageId = 5,PAGE_SHIFT = 13,5 >> 13 = 40960(字节)
//整数值 40960 表示内存中的一个地址位置,通过 (char*) 显示类型转换后,start 就指向了这个内存地址,即spa的起始地址
size_t bytes = span->_n << PAGE_SHIFT;//n表示管理的页的个数,计算该span的大小
char* end = start + bytes;//end指向span的结束地址
//将大块内存切成自由链表链接起来(采用尾插,使得即使被切割但在物理上仍为连续空间)
//1、先切下来一块作为头结点,便于尾插
span->_freelist = start;//PageCache申请下来的span是没有自由链表的所以需要让span的_freelist指向原来start指向的位置,现在我们要将其管理的空间划分为自由链表上的一个个结点
start += size;
void* tail = span->_freelist;
//循环尾插
while(start < end)
{
NextObj(tail) = start;//当前tail指向的内存块的前4/8个字节存放下一个结点的起始地址
tail = NextObj(tail);
start += size;
}
list.PushFront(span);//将获取到的span链接到CentralCache中的自由链表中
return span;//此时该span已经放在了CentralCache的某个桶的SpanList中了,返回的span可以直接使用
}
注意事项:PageCache每次申请的span都是管理128个页的(当然也可以更多只是这里设为了128)然后在使用时才会将这管理128个页的span进行分裂
PageCache的实现
基本概念:PageCache中的每个桶是按span中管理的页数进行映射的(直接映址法),即i号桶中都是管理i页的span,且span不会进行切分,切分工作由分配给的CentralCache负责
注意事项:PageCache的初始状态为全空(上图只是为了方便演示,实际刚开始没有一个span),所以第一次运行时需要先向堆申请一个管理128个页的span,然后将其切分,如果PageCache不为空但是没有合适的span也会堆中申请
主体框架
基本概念:与CentralCache一样,都采用饿汉模式保证程序运行过程中一个进程只有一个PageCache类
class PageCache
{
public:
static PageCache* GetInstance()
{
return &_sInst;
}
//获取一个k页的span
Span* NewSpan(size_t k);
private:
SpanList _spanLists[NPAGES];
std::mutex _pageMtx;
PageCache() {};
PageCache(const PageCache&) = delete;
static PageCache _sInst;
};
-
std::mutex _pageMtx:用于为整个PageCache加锁,而不是之前CentralCache中的桶锁
-
NAGES:PageCache中桶的个数为129,但实际只用[1,128]这个范围的,0页span的桶不用
盗了下这位大佬的图(●ˇ∀ˇ●) :【项目】九万字手把手教你写高并发内存池
关于整体加锁的解释
...以下内容是我让AI制造的场景,便于理解为什么不用桶锁...
场景描述:假设有 4 个线程 T1、T2、T3 和 T4,这些线程各自的 ThreadCache 中的内存不足,因此它们需要向 CentralCache 申请新的内存块。然而,CentralCache 中的相关桶已经无法满足需求,因此这 4 个线程必须同时向 PageCache 申请新的 span。
内存分配需求
- T1 需要申请一个 4 页的 span。
- T2 需要申请一个 8 页的 span。
- T3 需要申请一个 2 页的 span。
- T4 需要申请一个 6 页的 span。
PageCache 中目前只有两个空闲的 span:一个 8 页的 span 和一个 16 页的 span。
桶锁机制下的操作流程
-
线程同时申请锁:
- T1、T2、T3 和 T4 同时尝试申请 PageCache 中不同桶的锁来获取内存。
- PageCache 使用桶锁机制,因此每个线程试图加锁并访问自己需要的内存桶。
-
锁争用与分配:
- T1 成功获取了一个桶的锁,并分配了 4 页的 span。此时,PageCache 中 8 页的 span 被分配了 4 页,剩余 4 页。
- T2 试图申请 8 页的 span,但发现其中的 4 页已经被 T1 占用,无法满足其需求。于是,T2 释放锁并尝试申请下一个可用的 span(即 16 页的 span)。
- T3 成功获取了另一个桶的锁,并分配了 2 页的 span。此时,16 页的 span 被分配了 2 页,剩余 14 页。
- T4 需要 6 页的 span,它试图申请 16 页的 span,并成功分配了 6 页。此时,16 页的 span 剩余 8 页。
-
复杂的锁释放与重新申请:
- T2 最初试图申请 8 页的 span 失败后,它释放了最初的锁,并重新尝试申请 16 页的 span。由于 T4 已经使用了 6 页,T2 可以使用剩下的 8 页的 span。这要求 T2 再次获取新的锁并进行分配。
- 由于所有线程都在并发运行,它们频繁地获取和释放不同桶的锁,这导致了大量的锁竞争和上下文切换。每次 T2 失败后,它都必须重新尝试申请下一个可用的 span,这种反复操作使得加解锁操作频繁发生。
-
频繁加解锁带来的问题:
- 锁争用:由于 T1、T2、T3 和 T4 在不同的时间点争用不同的锁,系统必须频繁地处理锁申请和释放。这种锁争用增加了每个线程在申请内存时的等待时间。
- 上下文切换:当一个线程无法获取所需的锁时,它会被阻塞,等待其他线程释放锁。这种情况下,系统可能会频繁地切换上下文,保存和恢复线程的执行状态,这不仅消耗 CPU 资源,还可能导致 CPU 缓存失效,从而降低系统的整体性能。
- 内存碎片化:多个线程同时尝试分配内存块,并且桶锁机制只允许线程在各自的桶中操作,可能会导致内存碎片化问题。例如,T2 的 8 页需求在多次尝试后,可能不得不分配两个不连续的 4 页 span,这会增加内存管理的复杂性和后续内存操作的开销。
-
总开销累积:
- 加解锁开销:在这种高并发场景下,频繁的锁操作增加了系统的锁管理开销,尤其是在多个线程频繁竞争同一资源的情况下。每次加锁和解锁都会引发系统调用或内核级别的操作,增加了 CPU 的负担。
- 系统瓶颈:当线程频繁争用资源并且系统必须处理复杂的分配和锁管理时,CPU 资源被大量消耗在锁的管理和上下文切换上,而不是实际的工作任务上。最终,这种场景可能成为系统的性能瓶颈,导致吞吐量下降和响应时间延长。
结论:在这个复杂的场景下,多个线程(T1、T2、T3 和 T4)同时向 PageCache 申请内存块,并且桶锁机制导致频繁的锁争用和上下文切换。这种情况会显著增加 CPU 的消耗,导致系统性能下降。频繁的加解锁操作不仅浪费了大量的 CPU 时间,还可能导致内存碎片化和系统瓶颈问题。在这种高并发环境下,考虑采用整体锁或者优化锁的粒度,可能会更好地平衡并发性能与锁管理的开销
Span的分裂
① 当Central Cache向Page Cache申请内存时,Page Cache先检查对应位置有没有span,如果没有则寻找一个管理更多页的span,然后将该span管理的页进行分裂(申请的是4页span但没有,找到了一个管理10页的span,就将该span分为一个管理4页和6页的span)
② 如果一直没找到则PageCache要使用mmap、brk或者是VirtualAlloc等方式向堆申请管理128页的span,然后再重复①中的过程
Span的合并
①每当Central Cache释放回一个span,则在PageCache中判断能否与其相邻的span合并成一个管理更多页的更大的span(必须相邻,因为span管理的是连续的内存),从而减少内碎片(若有一个页号为100,管理页数为10的span,则该span管理的页的范围是[100, 110],此时判断99和111页是否空闲,若空闲就将它们合并到这个span中,假设它俩均空闲,那么合并后span就变成了页号为99,管理页数为12的span,此时它的管理范围为[99, 111],之后一直重复上述步骤,不断往两边扩展,某一侧的相邻页不为空闲就停下,直到两侧的相邻页均不空闲就停止扩展)
获取新span
补充内容1
在SpanList类中新增Empty和PopFront函数
(Erase函数没有修改只是为了便于观察PopFront函数的功能)
//判断是否为空
bool Empty()
{
return _head->_next == _head;
}
//头删
Span* PopFront()
{
Span* front = _head->_next;//_head->_next指向的是那个有用的第一个结点而不是哨兵位
Erase(front);
return front;//删掉后就要用,所以要返回删掉的那块内存的地址
}
//删除指定位置的span(要还给page cache)
//位置描述:prev pos next
void Erase(Span* pos)
{
assert(pos);//指定位置不能为空
assert(pos != _head);//不能将头指针删除
//暂存一下位置
Span* prev = pos->_prev;
Span* next = pos->_next;
prev->_next = next;
next->_prev = prev;
}
补充内容2
将SystemAlloc函数从定长内存池的头文件ObjectPool.h中移到公共头文件Common.h中
//封装VirtualAlloc跳过malloc直接向操作系统申请以页为单位的内存
inline static void* SystemAlloc(size_t kpage)//kpage表示页数
{
#ifdef _WIN32//使用Windows开发环境时可以使用Windows提供的VirtualAlloc函数,记得包含<windows.h>文件才有它们三个的定义
void* ptr = VirtualAlloc(0, kpage << 13, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
#endif
if (ptr == nullptr)
throw std::bad_alloc();//抛异常
return ptr;
}
具体实现
切分规则:
- 切分成一个管理k页的span和一个n-k页的span
- 管理k页的span分配给CentralCache
- 管理n-k页的span挂到PageCache的第n-k号桶中
Span* PageCache::NewSpan(size_t k)
{
assert(k > 0 && k < NPAGES);
//先检查PageCache的第k个桶中有没有span,有就头删一个
if (!_spanLists[k].Empty())
{
return _spanLists->PopFront();
}
//检查该桶后面的大桶中是否有span,如果有就进行span1的分裂
for (size_t i = k + 1; i < NPAGES; ++i)//因为第一个要询问的肯定是k桶的下一个桶所以i = k + 1
{
//后续大桶有span,执行span的分裂
if (!_spanLists[i].Empty())
{
Span* nSpan = _spanLists[i].PopFront();
Span* kSpan = new Span;
//在nSpan头部切一个k页的span下来
kSpan->_PageId = nSpan->_PageId;
kSpan->_n = k;
nSpan->_PageId += k;
nSpan->_n -= k;
_spanLists[nSpan->_n].PushFront(nSpan);
return kSpan;
}
}
//走到这里就说明PageCache中没有合适的span了,此时就去找堆申请一个管理128页的span
Span* bigSpan = new Span;
void* ptr = SystemAlloc(NPAGES - 1);//NPAGES为129,我们要128页的内容,ptr存放分配的span的起始地址
bigSpan->_PageId = (size_t)ptr >> PAGE_SHIFT;//由地址计算页号,因为起始地址 = 页号 * 页大小,所以页号 = 起始地址 / 页大小,使用位运算更快
bigSpan->_n = NPAGES - 1;//新的大span中管理的页的数量为128个
_spanLists[bigSpan->_n].PushFront(bigSpan);
return NewSpan(k);//重新调用一次自己,那么此时PageCache中就有一个管理k页的span了,可以从PageCache中直接分配了,不需要再考虑该返回什么
}
NewSpan的返回值有点🐂了,反正我想不出来
加解锁
涉及加解锁的代码不再往前面的代码中加入了
请查看下面的文字和图片说明进行加解锁代码的添加
基本概念:适当的加解锁可以避免线程冲突问题
加锁位置1:FetchRangeObj中执行GetOneSpan前
解锁位置1:GetOneSpan中执行PageCache的加锁前
加锁位置2:GetOneSpan中执行NewSpan前
解锁位置2:GetOneSpan中执行NewSpan后
加锁位置3:GetOneSpan中执行PushFront前
解锁位置3:FetchRangeObj中执行返回actualNum前
~over~