高并发内存池(三):CentralCache与PageCache的实现

news2025/4/28 16:31:31

目录

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。

桶锁机制下的操作流程

  1. 线程同时申请锁

    • T1、T2、T3 和 T4 同时尝试申请 PageCache 中不同桶的锁来获取内存。
    • PageCache 使用桶锁机制,因此每个线程试图加锁并访问自己需要的内存桶。
  2. 锁争用与分配

    • 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 页。
  3. 复杂的锁释放与重新申请

    • T2 最初试图申请 8 页的 span 失败后,它释放了最初的锁,并重新尝试申请 16 页的 span。由于 T4 已经使用了 6 页,T2 可以使用剩下的 8 页的 span。这要求 T2 再次获取新的锁并进行分配。
    • 由于所有线程都在并发运行,它们频繁地获取和释放不同桶的锁,这导致了大量的锁竞争和上下文切换。每次 T2 失败后,它都必须重新尝试申请下一个可用的 span,这种反复操作使得加解锁操作频繁发生。
  4. 频繁加解锁带来的问题

    • 锁争用:由于 T1、T2、T3 和 T4 在不同的时间点争用不同的锁,系统必须频繁地处理锁申请和释放。这种锁争用增加了每个线程在申请内存时的等待时间。
    • 上下文切换:当一个线程无法获取所需的锁时,它会被阻塞,等待其他线程释放锁。这种情况下,系统可能会频繁地切换上下文,保存和恢复线程的执行状态,这不仅消耗 CPU 资源,还可能导致 CPU 缓存失效,从而降低系统的整体性能。
    • 内存碎片化:多个线程同时尝试分配内存块,并且桶锁机制只允许线程在各自的桶中操作,可能会导致内存碎片化问题。例如,T2 的 8 页需求在多次尝试后,可能不得不分配两个不连续的 4 页 span,这会增加内存管理的复杂性和后续内存操作的开销。
  5. 总开销累积

    • 加解锁开销:在这种高并发场景下,频繁的锁操作增加了系统的锁管理开销,尤其是在多个线程频繁竞争同一资源的情况下。每次加锁和解锁都会引发系统调用或内核级别的操作,增加了 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;
}

具体实现

切分规则:

  1. 切分成一个管理k页的span和一个n-k页的span
  2. 管理k页的span分配给CentralCache
  3. 管理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~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2147201.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

linux第三课(linux中安装nginx与redis及SpringBoot集成redis)

目录 一.nginx引入 二.关于nginx 1.什么是nginx 2.nginx的特点 3.在nginx中安装nginx 三.关于redis 1.背景引入 2.什么是redis 3.redis的特点 4.在linux下的docker中安装redis 四.redis中的数据结构 (1)String(字符串) (2)Hash (3)list(列表) (5)zset(sorted se…

1734. 解码异或后的排列

1. 题目 1734. 解码异或后的排列 2. 解题思路 要搞明白这个题目可以先来看下它的简化版题目&#xff1a;1720. 解码异或后的数组 [!NOTE] 题目&#xff1a; 未知 整数数组 arr 由 n 个非负整数组成。 经编码后变为长度为 n - 1 的另一个整数数组 encoded &#xff0c;其中 e…

ES6标准---【八】【学习ES6看这一篇就够了!!!】

目录 前言 export命令 输出变量 输出函数/类 export中的as别名 export必须一一对应 export接口的响应性 注意 import命令 import命令的语法 import命令里的as别名 import的只读性 import命令具有提升性 import的一些约定 import的静态执行 import的唯一执行性 模…

windows查找端口号被占用

在很多开发的时候&#xff0c;可能端口号有被占用的情况&#xff0c;导致项目打不开。 用下面这个命令即可&#xff1a; 比如我的3000端口被占用&#xff0c;我找找哪个进程在占用我的3000端口号

SVM原理

SVM 这里由于过了很长时间 博主当时因为兴趣了解了下 博主现在把以前的知识放到博客上 作为以前的学习的一个结束 这些东西来自其他资料上 小伙伴看不懂英文的自行去翻译下吧 博主就偷个懒了 多维空间和低维空间 不一样的分法&#xff0c;将数据映射到高维 &…

【电路笔记】-运算放大器比较器

运算放大器比较器 文章目录 运算放大器比较器1、概述2、表示2.1 同相比较器2.2 反相比较器3、临界点转换4、施密特触发器4.1 同相触发器4.2 反相触发器4.3 应用5、总结1、概述 在前面的大多数运算放大器文章中,电路都有一个到反相输入的反馈环路。 这种设计是最常见的,因为它…

汇总区间计算

给定一个 无重复元素 的 有序 整数数组 nums 。 返回 恰好覆盖数组中所有数字 的 最小有序 区间范围列表 。也就是说&#xff0c;nums 的每个元素都恰好被某个区间范围所覆盖&#xff0c;并且不存在属于某个范围但不属于 nums 的数字 x 。 列表中的每个区间范围 [a,b] 应该按…

感知笔记:ROS 视觉- 跟随红球

- 目录 - 如何在 ROS 中可视化 RGB 相机。如何作为机器人切换主题。如何创建 blob 检测器。如何获取要跟踪的颜色的颜色编码。如何使用 blob 检测数据并移动 RGB 相机以跟踪 blob。 机器人技术中最常见的传感器是不起眼的 RGB 摄像头。它用于从基本颜色跟踪&#xff08;blob 跟…

Vue2接入高德地图API实现搜索定位和点击获取经纬度及地址功能

目录 一、申请密钥 二、安装element-ui 三、安装高德地图依赖 四、完整代码 五、运行截图 一、申请密钥 登录高德开放平台&#xff0c;点击我的应用&#xff0c;先添加新应用&#xff0c;然后再添加Key。 如图所示填写对应的信息&#xff0c;系统就会自动生成。 二、安装…

python-奖金/贪心的小明

一&#xff1a;奖金 题目描述 企业发放的奖金根据利润提成。利润低于或等于 100000 元的&#xff0c;奖金可提 10%&#xff1b; 利润高于 100000 元&#xff0c;低于 200000 元&#xff08; 100000<I≤200000&#xff09;时&#xff0c;低于 100000 元的部分按 10% 提成&…

二叉树的链式结构和递归程序的递归流程图

二叉树的链式存储结构是指&#xff0c;用链表来表示一棵二叉树&#xff0c;即用链来指示元素的逻辑关系。 通常的方法是链表中每个结点由三个域组成&#xff0c;数据域和左右指针域&#xff0c;左右指针分别用来给出该结点左孩子和右孩子所在的链结点的存储地址 。链式结构又分…

一个基于VB的期刊信息管理系统

一个基本的期刊信息管理系统的示例&#xff0c;使用 Visual Basic (VB.NET) 编写。这个示例将展示如何创建一个简单的期刊信息管理系统&#xff0c;其中包括添加、查看、编辑和删除期刊的功能。 系统需求 添加期刊&#xff1a;允许用户输入期刊的信息&#xff08;如标题、作者…

面试官问:你如何处理与同事或上级的分歧?【无标题】

面试官问&#xff1a;你如何处理与同事或上级的分歧&#xff1f; 当面试官问你如何处理与同事或上级的分歧&#xff0c;其实面试官的目的是评估你的沟通技巧、冲突解决能力和团队合作的能力。在一起共事&#xff0c;就一定有分歧发生&#xff0c;有争执是正常的&#xff0c;关…

OpenCV 1

前言&#xff1a;开新坑辽&#xff0c;&#xff0c; 目录 计算机眼中的图像 视频的读取与处理 ROI区域 边界填充 数值计算 腐蚀操作 膨胀操作 开运算与闭运算 梯度计算 礼貌与黑帽 Sobel算子 梯度计算方法 scharr与laplacian 计算机眼中的图像 灰色图片&#xff0…

ROS第四梯:ROS项目中添加自定义类

第一步&#xff0c;ROS项目结构介绍 工作空间中包含一个名为pcl_ros_test的功能包&#xff0c;其中main.cpp是原有项目自带的&#xff0c;接下来以CommonAlg自定义类添加为例进行介绍。 第二步&#xff1a;头文件CommonAlg.h创建和编写&#xff0c;并保存在include/pcl_ros_tes…

达梦数据库导入xml迁移到达梦数据库大文件导致中断问题解决方案记录?

问题&#xff1a;我将同事给我的xml文件迁移到盗梦数据库&#xff0c;xml文件大约2G&#xff0c;在导入过程中&#xff0c;总是导入一半都不到就失败了。 原因&#xff1a;我的原因是我的电脑的系统的运行内存是16G的&#xff0c;后来我发现在没导入之前&#xff0c;其他进程已…

GUI编程18:文本框、密码框、文本域

视频链接&#xff1a;20、文本框、密码框、文本域_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1DJ411B75F?p20&vd_sourceb5775c3a4ea16a5306db9c7c1c1486b5 1.文本框 示例代码&#xff1a; package com.yundait.lesson06;import javax.swing.*; import java.a…

初始分布式系统和Redis特点(

&#xff08;一&#xff09;认识redis Redis是一个开源&#xff08;BSD许可&#xff09;&#xff0c;内存存储的数据结构服务器&#xff0c;可用作数据库&#xff0c;高速缓存和消息队列代理。它支持字符串、哈希表、列表、集合、有序集合&#xff0c;位图&#xff0c;hyperlog…

『功能项目』QFrameWorkBug修改器界面【65】

我们打开上一篇64QFrameWork道具栏物品生成的项目&#xff0c; 本章要做的事情是做一个道具bug调试面板&#xff0c;可以增加主角属性&#xff0c;可以增加道具的功能 首先创建一个空物体&#xff08;钉子&#xff09; 按住Alt键将空物体钉到左侧 重命名为Left 创建Button、Im…

基于SpringBoot+Vue的企业会议室预定管理系统

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、SSM项目源码 系统展示 【2025最新】基于JavaSpringBootVueMySQL的…