4_高并发内存池项目_高并发池内存释放设计_ThreadCache/CentralCache/PageCache回收并释放内存

高并发池内存释放设计

对各缓存层释放内存的设计,不仅仅是从上一层回收内存,还包括对回收回来的内存怎样处理更有利于下一缓存层的回收,提高效率。

高并发内存池内存释放步骤:

线程对象释放内存

↓↓↓↓↓
ThreadCache(1.回收线程释放的内存,2.将多余内存整理归还CentralCache)

↓↓↓↓↓
CentralCache(1.回收ThreadCache释放的内存,2.将多余Span归还给PageCache)

↓↓↓↓↓
PageCache(1.回收CentralCache释放的Span,2.合并,3.并且将内存归还操作系统)

ThreadCache回收内存的标准

  1. ThreadCache回收线程释放的内存。
    从上一层,也就是申请内存的对象释放内存时,将被释放的内存进行回收,也就是将空闲的内存块回收,挂在每个对应的哈希桶中。
  2. ThreadCache释放内存给CentralCache回收。
    当某个哈希桶中的自由链表存储的内存到达什么程度时,将其整理回收给CentralCache?
  • tcmalloc考虑了两个方向,一个是链表的长度,一个是内存的大小(比如一个ThreadCache超过2MB或者多少就回收)。
  • 我们这里只考虑链表的长度,链表的长度超过了它一次向CentralCache申请的批量对象个数时,将这个哈希桶中批量数量的内存还给CentralCache。
  • 首先,我们要比较一下链表长度(_freelists[index].Size() > _freelists[index].MaxSize()),长度超过时,我们找到要还给CentralCache的一段内存,由start,end,size(当时批量的数量),我们要将这段被释放的内存还给CentralCache。这里要调用CentralCache回收ThreadCache释放的内存的函数。

CentralCache回收释放内存

1.从ThreadCache中回收内存,根据对应的单个内存大小,计算出对应的哈希桶,将它们放在哈希桶中对应的Span下。如何得到内存块对应的Span?

2.空闲的Span被PageCache回收,回收的标准是什么?

PageCache回收释放内存

  1. PageCache如何处理从CentralCache中回收的Span?
  2. PageCache释放内存到操作系统。
// 封装线程释放内存
static void concurrentFree(void* obj)
{// 现在通过对应的指针找到对应的Span,通过Span找到对应的size也就是申请的内存大小Span* span = PageCache::GetInstance().MapObjectToSpan(obj);size_t size = span->_objSize;if (size > MAX_BYTES){// 访问PageCache中的ReleaseSpanToPageCachePageCache::GetInstance()._pageMtx.lock();PageCache::GetInstance().ReleaseSpanToPageCache(span);PageCache::GetInstance()._pageMtx.unlock();}else{assert(pTLSThreadCache);// 回收内存对象释放的内存pTLSThreadCache->ThreadFree(obj, size);}
}
/* 系统申请释放内存 */
#ifdef _WIN64  
#include <Windows.h>  
#elif defined(_WIN32) && !defined(_WIN64)  /* 检查是否定义了 _WIN32 宏但没有定义 _WIN64 宏,* 这适用于32位Windows系统。* 如果条件满足,也包含 <Windows.h> 头文件。*/
#include <Windows.h>  
#else  
#error "Unsupported platform"  //#else 和 #error 指令用于捕获不支持的平台,并在编译时生成错误。
#endif // 直接去堆上申请按页申请空间
/* 在Windows下,可以调用VirtualAlloc函数;在Linux下,可以调用brk或mmap函数* 使用条件编译,根据不同的平台选择不同的头文件* _WIN32 宏在32位和64位Windows上都定义,* 因此单独检查 _WIN32 而不排除 _WIN64 ,会导致在64位Windows上也执行32位分支的代码* 或者 不判断定义64位,将_WIN64放在_WIN32前也可以*/
inline static void* SystemAlloc(size_t kpage)
{
#ifdef _WIN64void* ptr = VirtualAlloc(0, kpage * (1 << 13), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);#elif  _WIN32void* ptr = VirtualAlloc(0, kpage * (1 << 13), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
#else// 由于前面的#error,这行代码实际上不会执行  // 但为了保持函数完整性,这里保留了返回NULL的语句  return NULL;
#endifif (ptr == nullptr)throw std::bad_alloc();return ptr;
}inline static void SystemFree(void* ptr)
{
#ifdef _WIN64VirtualFree(ptr, 0, MEM_RELEASE);
#elif	_WIN32VirtualFree(ptr, 0, MEM_RELEASE);
#endif
}

一、ThreadCache–回收释放内存

/* ThreadCache.h */
#pragma once
#include "CommonPool.h"class ThreadCache
{
private:FreeList _freeLists[NFREELISTS];// 哈希桶public:// 需要提供两个接口  一个是申请对象空间,一个是释放对象空间// 申请内存对象void* ThreadAlloc(size_t size);// 释放内存对象void ThreadFree(void* obj, size_t size);// 链表过长时,回收内存回到中心缓存// 回收一段长度为size的链表回到中心缓存void ListTooLong(FreeList& list, size_t size);// 从Cental Cache中申请缓存void* FetchFromCentralCache(size_t index, size_t alignSize);
};// TLS thread local storage(线程局部存储、线程本地存储)
static _declspec(thread) ThreadCache* pTLSThreadCache = nullptr;
// thread 用于声明一个线程本地变量, 
// _declspec(thread)的前缀是Microsoft添加给Visual C++编译器的一个修改符。

怎么判断什么时候把内存还给CentralCache?还多少内存?

当释放某个线程申请的对象内存时,ThreadCache将该对象插入到对应哈希桶的自由链表当中。

但随着线程不断的释放,对应自由链表的长度也会越来越长,此时我们应该将内存还给CentralCache,避免内存堆积造成的浪费回收一部分内存对象到CentralCache

tcmalloc考虑了两个方面:一个是链表的长度,一个是内存的大小。

这里我们只考虑链表的长度。

如果ThreadCache某个桶当中自由链表的长度超过它一次向CentralCache批量申请的对象个数时,就要把该自由链表当中的这些对象还给CentralCache。

接着完善_freeList的结构,我们需要获得对应哈希桶中自由链表的长度,以及在回收这些内存时的操作函数,删除某段链表中的一部分。

关于_freeLists[index].Size()中的变量_size在哪些位置发生变化?

_freeLists[index].Size()是ThreadCache的哈希桶中申请到的链表长度,也就是向CentralCache申请释放内存块后发生变化。

class FreeList
{
public:// 线程申请内存就是从对应的自由链表中取出内存块(头删)void Push(void* obj){NextObj(obj) = _freeList;_freeList = obj;++_size;}// 线程释放内存就是将内存块释放到对应的自由链表中(头插)void* Pop(){assert(_freeList);// 自由链表为空时,无法取出空闲内存块void* obj = _freeList;_freeList = NextObj(_freeList);--_size;return obj;}// 判断自由链表中是否存在内存bool Empty(){return _freeList == nullptr;}// 获取到_MaxSize成员变量size_t& MaxSize(){return _MaxSize;}size_t Size(){return _size;}// 插入一定范围的内存块void PushRange(void* start, void* end, size_t num){NextObj(end) = _freeList;_freeList = start;_size += num;}void PopRange(void*& start, void*& end, size_t n){assert(n <= _size);start = _freeList;end = start;for (size_t i = 0; i < n; ++i){end = NextObj(end);}_freeList = NextObj(end);NextObj(end) = nullptr;_size -= n;}private:void* _freeList = nullptr;//在FreeList结构中增加一个叫做_MaxSize的成员变量,用于慢开始反馈调节size_t _MaxSize = 1;size_t _size = 0;
};
/* ThreadCache.cpp */// 释放对象时,链表长度过长,回收内存到中心缓存(central cache)
void ThreadCache::ListTooLong(FreeList& list, size_t size)
{// 拿一个批量的,不是全部// _freeLists[index].Size() > _freeLists[index].MaxSize()void* start = nullptr;void* end = nullptr;// 还给对应的central cache 中的哈希桶list.PopRange(start, end, list.MaxSize());		// 输出型参数//调用central cache回收thread cache释放的内存的函数CentralCache::GetInstance().ReleaseListToSpans(start, list.MaxSize());	
}void ThreadCache::ThreadFree(void* obj, size_t size)
{// 释放的内存 挂回 _freeList 链表中assert(obj);// 首先确保释放的对象存在assert(size <= MAX_BYTES);// 内存挂回;链表中size_t index = SizeClass::Index(size);_freeLists[index].Push(obj);/* 如果Thread Cache某个桶当中自由链表的长度超过它一次批量向central cache申请的对象个数,那么此时我们就要把该自由链表当中的这些对象还给central cache。 */if (_freeLists[index].Size() > _freeLists[index].MaxSize()){ListTooLong(_freeLists[index], size);}
}

二、CentralCache–回收释放内存

大体思路:

  1. CentralCache释放内存有两个大方向,一个是从ThreadCache中回收内存块,一个是回收后的内存块使得部分Span处于完全空闲状态,可以释放给PageCache,增加空间利用;
  2. 回收从ThreadCache中释放的内存:当ThreadCache中某一自由链表下悬挂的空闲内存块过多时由CentralCache进行回收RealseListToSpans(),这些内存块回收到对应哈希桶中的对应的Span中。因此在分割Span为多个内存块时就可以将内存块的页号与Span做映射关系确保回收的内存块可以找到对应的Span
  3. 将完全空闲的Span释放给PageCache:通过_usecount这个变量来判断Span是否是空闲的,确认空闲后,就释放给PageCache。

(一)创建页号与Span之间的额映射关系

通过对应的哈希桶和span。将对应的释放内存对象挂在对应的哈希桶下的span中。

我们可以通过内存块的起始位置获得是Span的第几页。起始地址>>13(8kb)得到_pageid,然后遍历spanlist,但是这个方法显然过于复杂。

( 注意这里 画的图只是为了好理解,直观看到联系。实质上,内存的申请不是真正的划分内存空间和移动内存位置,它的地址和位置是不会改变的,我们的还回也就是找到对应的地址,再把它们连接起来 )

我们要还到对应的span下。那么可以创建页号(_pageId)与Span的映射关系页号(_pageId)可以计算出Span的起始地址,通过这个页号和页数,我们可以找到Span中每一页的位置,也就是可以定位到 切分的内存对象所处在哪个Span的哪个页中:

**unordered_map_pageId + i , Span>**建立起联系。

在创建Span时就通过映射关系,将页号与Span映射起来。而创建Span的过程中是在PageCache中,
这个映射关系设置为 :

class PageCache
{
private:SpanList spanPageLists[NPAGES]; // 0号桶不使用,其他一一对应。std::unordered_map<PageID, Span*> _idSpanMap;private:// 静态私有成员变量static PageCache _instance;// 私有构造函数PageCache(){}PageCache(const PageCache&) = delete;PageCache& operator=(const PageCache&) = delete;public:// 加锁std::mutex _pageMtx;// 公有静态访问方法static PageCache& GetInstance(){return _instance;}// 申请一个新的SpanSpan* NewSpan(size_t k);
};

我们可以把这个映射关系设置在PageCache中,在创建一个Span时,通过它的页数,将Span中的每一页与Span创建上联系。

// 访问PageCache时记得加锁
Span* PageCache::NewSpan(size_t k)
{// PageCache对应哈希桶中的Span是否存在if (!spanPageLists[k].Empty()){Span* kSpan =  spanPageLists[k].PopFront();for (PageID i = 0; i < kSpan->_n; ++i){_idSpanMap[kSpan->_pageId + i] = kSpan;}return kSpan;}else{// 往上找大页,切割for (size_t i = k + 1; i < NPAGES; ++i){if(!spanPageLists[i].Empty()){ // 不为空,切割Span* kSpan = new Span;Span* nSpan = spanPageLists[i].PopFront();kSpan->_pageId = nSpan->_pageId;kSpan->_n = k;nSpan->_n -= k;nSpan->_pageId += k;for (PageID i = 0; i < kSpan->_n; ++i){_idSpanMap[kSpan->_pageId + i] = kSpan;}spanPageLists[nSpan->_n].PushFront(nSpan);return kSpan;}}// 没有大页,就向系统申请Span* bigSpan = new Span;void* ptr = SystemAlloc(NPAGES-1);bigSpan->_pageId =(PageID) ptr >> PAGE_SHIFT;bigSpan->_n = NPAGES - 1;spanPageLists[bigSpan->_n].PushFront(bigSpan);return NewSpan(k);}
}

此时此刻我们将CentralCache会申请到的Span与其页数创建了映射关系。

(二)通过创建的映射关系将内存块释放回对应的Span

建立_pageId与Span的映射,方便CentralCache回收小块内存时,查找对应的Span。

通过创建的页号与Span的映射关系找到对应的Span

在PageCache类中添加一个函数接口MapObjectToSpan(obj),通过对象的地址找到该对象对应的Span了,直接将该对象的地址除以页的大小得到页号,然后在unordered_map当中找到其对应的Span,让CentralCache获取这里的映射关系。

我们有指向该对象的指针,也就是可以得到了它的地址,通过这份地址,我们可以求出该对象的页号:

 PageID id = (PageID)obj >> PAGE_SHIFT;

通过映射关系得到它属于哪个Span:

**auto ret = _idSpanMap.find(id);**

每个Span含有页号范围都是不一样的,无需担心出错。这个Span的页号范围(比如:[n~n+5])。

通过查到对象的页号,在映射关系中指向哪个Span。

因为这个对象是通过Span申请出来的,所以一定能通过页号找到对应的Span。

/* Pagecache.cpp */
// 获取从对象到span的映射
// 页号和span的映射
// std::unordered_map<PageID, Span*> _idSpanMap;
Span* PageCache::MapObjectToSpan(void* obj)
{// 页号和span的映射PageID id = (PageID)obj >> PAGE_SHIFT; // 通过对象的地址得到页号// 找到使用_idSpanMap.find(id);在映射表中查找该页号对应的Span对象。// 是一定能找到的,不然我们的代码是有问题的auto ret = _idSpanMap.find(id);		  // 如果找到了(即ret != _idSpanMap.end()),则返回该Span对象的指针。if (ret != _idSpanMap.end())  {return ret->second;}else{assert(false);return nullptr;}
}

搞清楚了上面的部分,现在我们要开始回收ThreadCache中过长的链表了。

(三)回收ThreadCache中过长的链表

现在我们可以得到对象属于哪个Span了。

回收内存对象,要做的就是将这块内存对象头插进入对应的Span中。

/* central cache.cpp */// CentralCache回收ThreadCache释放的内存
void CentralCache::RealseListToSpans(void* start, size_t size)
{size_t index = SizeClass::Index(size);// 加锁_spanLists[index]._mtx.lock();// 释放的内存对象 不一定是 一个Span的while (start){// 通过对象获取到它是哪个span MapObjectToSpanvoid* next = NextObj(start);Span* span = PageCache::GetInstance().MapObjectToSpan(start);NextObj(start) = span->_list;span->_list = start;start = next;}// 解锁_spanLists[index]._mtx.unlock();
}

(四)CentralCache释放内存

接下来我们要考虑的是如何判断CentralCache什么时候释放内存?

在框架设计以及前面的大体思路处我们都提到了一个变量:

// 这时候就有我们在创建Span时出现的 一个变量
// Span中被利用的内存块数size_t _usecount = 0;	  
// 切好的小块内存被使用的使用计数,==0 说明所有对象都回来了

我们可以用 _usecount来判断spanlist中的某个Span是否是空闲的、未被申请内存的,如果是,可以释放给PageCache。

我们现在看看哪些位置的操作会影响到这个变量,在代码中对这个变量进行操作比如++,–。(这个变量在ThreadCache申请和释放内存时会发生变化,因此我们需要完善这部分相关的内容)

// ThreadCache从CentralCache中获取内存块
size_t CentralCache::FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t  size)
{// 获取一个非空Spansize_t index = ClassSize::Index(size);// 从现在开始访问CentralCache的哈希桶,需要开始加锁进入_spanLists[index]._mtx.lock();Span* span = CentralCache::GetOneSpan(_spanLists[index],size);assert(span);assert(span->_list);start = span->_list;end = start;// 设置一个变量n,和一个实际可申请对象数量的变量actualNum,以及别忘了batchNumsize_t n = 0, actualNum = 1;while (n < batchNum - 1 && NextObj(end) != nullptr){end = NextObj(end);++n;++actualNum;}// 把切割掉内存后的内存对象连接上,被切割下的内存尾端指向nullptrspan->_list = NextObj(end);NextObj(end) = nullptr;span->_usecount += actualNum;_spanLists[index]._mtx.unlock();return actualNum;
}
// CentralCache回收ThreadCache释放的内存
void CentralCache::ReleaseListToSpans(void* start, size_t size)
{size_t index = SizeClass::Index(size);// 加锁_spanLists[index]._mtx.lock();// 释放的内存对象 不一定是 一个span的 while (start){// 通过对象获取到它是哪个Span --MapObjectToSpanvoid* next = NextObj(start);Span* span = PageCache::GetInstance().MapObjectToSpan(start);NextObj(start) = span->_list;span->_list = start;span->_usecount--;// 从thread cache回收的内存已经回到了Span中。// 判断这个Span是否空闲,如果空闲,将这个Span归还给PageCache// 这时候就需要用到 _usecount 这个变量,判断Span中没有被利用,被申请掉的内存if (span->_usecount == 0){// 将这个要归还的span从链表中取出,删除// 保持链表的连续性_spanLists->Erase(span);span->list = nullptr;span->_next = nullptr;span->_prev = nullptr;PageCache::GetInstance().ReleaseSpanToPageCache(span);// 这时候,我们又是从CentralCache跳转到访问PageCache,思考是否需要加锁// 加锁加谁的锁?// 加锁加在什么位置?}start = next;}// 解锁_spanLists[index]._mtx.unlock();
}

PageCache::GetInstance().ReleaseSpanToPageCache(span);这段代码中我们又是从CentralCache跳转到访问PageCache,思考是否需要加锁?

加锁加在什么位置?

        _spanLists[index]._mtx.unlock();PageCache::GetInstance()._pagemtx.lock();PageCache::GetInstance().ReleaseSpanToPageCache(span);// 这时候,我们又是从central cache跳转到访问page cache,先解锁,在加锁,这时候防止其他线程阻塞PageCache::GetInstance()._pagemtx.unlock();_spanLists[index]._mtx.lock();

释放Span给PageCache时,需要加上PageCache的锁,也需要解去桶锁。

三、PageCache–回收释放内存

  1. PageCache回收CentralCache释放的内存,无可合并的Span,将释放回的Span挂在对应哈希桶中;
  2. PageCache中有可合并的相邻的Span,合并直到无相邻Span或最大为128页后不再合并,减少内存碎片问题。合并的条件为 _usecount=0,_isUse = false,使用 bool _isUse = false; 来判断Span是否处于PageCache的内存空间中。;
  3. PageCache释放内存给操作系统。

(一)kSpan的前后合并

从物理空间来看:

我们无法直接判断kSpan的前后页位于三层级的哪一处,这段空间并不是像我们画图那样,分给CntralCache的Span就被CentralCache拿走,不在PageCache中。实际上,这部分物理空间是始终存在,不会出现切割和移动位置的情况,也就是说这段大块的内存空间中,我们无法知晓在ThreadCache,CentralCache,PageCache是怎样占据这份物理空间的。

也就是说我们无法判断kspan的前后页是否是可以合并的部分,是否位于PageCache的空间中。

为什么不用_usecount判断?

首先看哪些存在会影响_usecount的值,在ThreadCache申请向CentralCache申请内存时,没有足够的内存,于是CentralCache向PageCache申请一个新的Span

但在申请的间隙,ThreadCache仍然可以释放内存给CentralCache,造成释放的内存足够前一步申请的内存使用。这时新申请来的空间未被利用,_usecount==0,又会被收回去合并。

而通过_isUse在CentralCache向PageCache申请一个新的Span时,它的状态就变为了true。

创建PageCache中未分配给CentralCache的Span页号与Span之间的映射关系

我们在前面为了方便回收ThreadCache中的小块内存对象,在kSpan分配给CentralCache时,就建立了各个页号与Span之间的映射关系。但实际上PageCache中还有nSpan悬挂在对应的哈希桶上,而这类nSpan并没有将自身的页号与Span做映射,因此这里我们要进一步对其操作,但是与kSpan的每一页都要和Span做映射不同,nSpan只要提供首尾页与Span做映射即可,用于合并前后页。

合并Span的要求,在前面提到过:

  1. Span在合并时,必须是前后页合并,可以通过一个Span的页号_pageId得到它前后页的页号_pageId,再通过前后页的页号_pageId映射关系找到对应的Span;
  2. 通过_isUse = false来进行判断Span在PageCache对象空间内,才可以对前后页进行合并。

我们要创建PageCache中nSpan的页数和Span的映射关系,就需要在PageCache申请内存时操作,当申请k页的Span时,如果是将n页的Span切成了一个k页的Span和一个n-k页的Span,我们除了需要建立k页span中每个页与该Span之间的映射关系(CentralCache中的span每一页都要映射)之外,还需要建立剩下的n-k页的Span与其首尾页(PageCache中的Span只需要映射首尾)之间的映射关系。

    // 后续的桶中有Span可以切分使用for (int i = k + 1; i < NPAGES; ++i){if (!_spanlists[i].Empty()){Span* kspan = new Span;					// 这个是最终要返回的切分好大小的SpanSpan* nspan = _spanlists[i].PopFront(); // 这个是作为切割用的Spankspan->_pageId = nspan->_pageId;		// 页号kspan->_n = k;							// 页数nspan->_pageId += k;nspan->_n -= k;// span切好了,要记得插入进去链表中_spanlists[nspan->_n].PushFront(nspan);//存储nSpan的首尾页号与nSpan之间的映射,方便PageCache合并Span时进行前后页的查找_idSpanMap[nspan->_pageId] = nspan;_idSpanMap[nspan->_pageId + nspan->_n - 1] = nspan;for (PageID i = 0; i < kspan->_n; ++i){_idSpanMap[kspan->_pageId + i] = kspan;}return kspan;	// 给CentralCache}}// 后续的桶中没有Span可以用,要向系统申请Span* bigSpan = new Span;void* ptr = SystemAlloc(NPAGES - 1); // 系统申请内存// 获取bigspan的 页号 和 页数bigSpan->_pageId = (PageID)ptr >> PAGE_SHIFT;bigSpan->_n = NPAGES - 1;// 插入_spanlists[bigSpan->_n].PushFront(bigSpan);// 重复切分128号桶为 k页和 128-k页// 重复上列切分步骤return NewSpan(k);}

(二)PageCache回收内存

PageCache回收内存后要对Span进行合并。

要想合并Span需要满足三个条件(这是从CentralCache):

一是_usecount==0(在ThreadCache释放内存,CentralCache回收内存时,当其中的某个Span中的内存对象均空闲时,将被PageCache回收内存);

二是_isUse=false,PageCache中的Span是未使用的;

三是合并起来最大是128页。

// 回收CentralCache中的空闲Span
void PageCache::ReleaseSpanToPageCache(Span* span)
{// 合并span的前后页,解决外碎片问题// 1.向前合并while (1){// Span的前一页 对应的Span(在PageCache中存在)PageID prevId = span->_pageId - 1;auto ret = _idSpanMap.find(prevId);if (ret == _idSpanMap.end()){break;	// 没有前一页,无法合并,跳出循环}// 有前一页,判断是否被使用中// span的页号为前一页对应span的首页号,合并页数Span* prevSpan = ret->second;if (prevSpan->_isUse == true){break;}// 有前一页,没有被使用中,合并后是否大于128页if (prevSpan->_n + span->_n > NPAGES - 1){break;}// 有前一页,没有被使用,合并后小于128页-->可以合并span->_pageId = prevSpan->_pageId;span->_n += prevSpan->_n;//将prevSpan从对应的双链表中移除_spanlists[prevSpan->_n].Erase(prevSpan);delete prevSpan;}//2、向后合并while (1){PageID nextId = span->_pageId + span->_n;auto ret = _idSpanMap.find(nextId);// 没有后面的页号(还未向系统申请),停止向后合并if (ret == _idSpanMap.end()){break;}// 后面的页号对应的span正在被使用,停止向后合并Span* nextSpan = ret->second;if (nextSpan->_isUse == true){break;}// 合并出超过128页的span无法进行管理,停止向后合并if (nextSpan->_n + span->_n > NPAGES - 1){break;} // 有后一页,没有被使用,合并后小于128页-->可以合并span->_n += nextSpan->_n;// 将nextSpan从对应的双链表中移除_spanlists[nextSpan->_n].Erase(nextSpan);delete nextSpan;}// 将合并后的span挂到对应的双链表当中_spanlists[span->_n].PushFront(span);// 建立新的映射 -> 该span与其首尾页的映射_idSpanMap[span->_pageId] = span;_idSpanMap[span->_pageId + span->_n - 1] = span;// 将该span设置为未被使用的状态span->_isUse = false;
}

从CentralCache来的Span,满足上面一,二的条件后,可以查找PageCache中是否有其前后页。

  1. 首先找到Span前后页对应的Span是否存在,分别命名为prevSpan,nextSpan。(注意这里我们只是找到了Span的前后页,并不确定它们是否在PageCache对象空间);
  2. 其次,要判断prevSpan,nextSpan是否在PageCache对象空间中。通过_isUse==false来判断是否被利用,如果没有被利用,那么他们在PageCache对象空间中,可与从CentralCache中回收的Span进行合并操作;
  3. PageCache中最大的页数就是128页,如果前后页合并Span相加大于128页,是不合理的,因此我们需要加上合并后是否大于128的判定条件;
  4. 最后,满足上列所有条件后,得到的就是一个合并后的Span。这时候要注意对这个新的Span进行必要的处理,比如它的映射关系,也就是注意更新span自身的信息。
  5. prevSpan或者nextSpan原来的位置还插在双向链表中,如果不删除他们,申请(或回收)时他们可能会在被重复利用(或合并),会造成错误。
  6. 新合并来的Span,我们根据(span->_n)将它插入进对应的page页中,(如果从CentralCache回收的Span为什么不做处理,那是因为它从CentralCache中切割后就做了处理)

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

centos9编译安装opensips 二【进阶篇-定制目录+模块】推荐

环境&#xff1a;centos9 last opensips -V version: opensips 3.6.0-dev (x86_64/linux) flags: STATS: On, DISABLE_NAGLE, USE_MCAST, SHM_MMAP, PKG_MALLOC, Q_MALLOC, F_MALLOC, HP_MALLOC, DBG_MALLOC, CC_O0, FAST_LOCK-ADAPTIVE_WAIT ADAPTIVE_WAIT_LOOPS1024, MAX_RE…

分子动力学模拟里的术语:leap-frog蛙跳算法和‌Velocity-Verlet算法

分子动力学模拟&#xff08;Molecular Dynamics Simulation&#xff0c;简称MD&#xff09;是一种基于经典力学原理的计算物理方法&#xff0c;用于模拟原子和分子在给定时间内的运动和相互作用‌。以下是关于分子动力学模拟的一些核心术语和概念&#xff1a; ‌定义系统‌&am…

iOS开发设计模式篇第二篇MVVM设计模式

目录 一、什么是MVVM 二、MVVM 的主要特点 三、MVVM 的架构图 四、MVVM 与其他模式的对比 五、如何在iOS中实现MVVM 1.Model 2.ViewModel 3.View (ViewController) 4.双向绑定 5.文中完整的代码地址 六、MVVM 的优缺点 1.优点 2.缺点 七、MVVM 的应用场景 八、结…

【C++图论 并集查找】2492. 两个城市间路径的最小分数|1679

本文涉及知识点 C图论 并集查找&#xff08;并查集) LeetCode2492. 两个城市间路径的最小分数 给你一个正整数 n &#xff0c;表示总共有 n 个城市&#xff0c;城市从 1 到 n 编号。给你一个二维数组 roads &#xff0c;其中 roads[i] [ai, bi, distancei] 表示城市 ai 和 …

Linux应用编程(五)USB应用开发-libusb库

一、基础知识 1. USB接口是什么&#xff1f; USB接口&#xff08;Universal Serial Bus&#xff09;是一种通用串行总线&#xff0c;广泛使用的接口标准&#xff0c;主要用于连接计算机与外围设备&#xff08;如键盘、鼠标、打印机、存储设备等&#xff09;之间的数据传输和电…

⽤vector数组实现树的存储(孩⼦表示法)c++

在我们遇到的算法题中&#xff0c; ⼀般给出的树结构都是有编号的&#xff0c;这样会简化我们之后存储树的操作 &#xff0c;⼀般提供两个信息&#xff1b; 结点的个数 n;n-1条x结点与y结点相连的边 题⽬描述: ⼀共9个结点셈 1号结点为根节点&#xff0c;接下来8⾏&#xff…

一个基于Python+Appium的手机自动化项目~~

本项目通过PythonAppium实现了抖音手机店铺的自动化询价&#xff0c;可以直接输出excel&#xff0c;并带有详细的LOG输出。 1.excel输出效果: 2. LOG效果: 具体文件内容见GitCode&#xff1a; 项目首页 - douyingoods:一个基于Pythonappium的手机自动化项目&#xff0c;实现了…

基于微信小程序的童装商城的设计与实现(LW+源码+讲解)

专注于大学生项目实战开发,讲解,毕业答疑辅导&#xff0c;欢迎高校老师/同行前辈交流合作✌。 技术范围&#xff1a;SpringBoot、Vue、SSM、HLMT、小程序、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、安卓app、大数据、物联网、机器学习等设计与开发。 主要内容&#xff1a;…

方便快捷的软件展示平台查找和下载所需的软件

## 软件展示平台项目概述 背景&#xff1a; 随着互联网的发展&#xff0c;软件的数量日益增长&#xff0c;用户需要一款方便快捷的软件展示平台来查找和下载所需的软件。本软件展示平台旨在为用户提供一个集中展示各类软件的平台&#xff0c;方便用户快速找到所需的软件并进行…

进程、线程和协程的区别

进程、线程和协程的区别 在操作系统中&#xff0c;进程、线程 和 协程 是并发编程中的核心概念。 1. 进程 定义 进程是程序的一次执行过程&#xff0c;是操作系统进行资源分配和调度的基本单位。每个进程都有自己独立的地址空间和系统资源。 特点 独立性&#xff1a;每个…

MinIO的安装与使用

目录 1、安装MinIO 1.1 下载 MinIO 可执行文件 1.2 检查 MinIO 是否安装成功 1.3 设置数据存储目录 1.4 配置环境变量&#xff08;可选&#xff09; 1.5 编写启动的脚本 1.6 开放端口 1.7 访问 2、项目实战 2.1 引入依赖 2.2 配置yml文件 2.3 编写Minio配置类 2.4…

CSDN 博客之星 2024:默语的技术进阶与社区耕耘之旅

CSDN 博客之星 2024&#xff1a;默语的技术进阶与社区耕耘之旅 &#x1f31f; 默语&#xff0c;是一位在技术分享与社区建设中坚持深耕的博客作者。今年&#xff0c;我有幸再次入围成为 CSDN 博客之星TOP300 的一员&#xff0c;这既是对过往努力的肯定&#xff0c;也是对未来探…

土壤墒情中土壤 pH 值的监测方法与意义

土壤&#xff0c;作为农作物生长的根基&#xff0c;其质量对农业生产有着深远影响。在衡量土壤质量的众多指标中&#xff0c;土壤 pH 值是极为关键的一项。它不仅反映了土壤的酸碱度&#xff0c;还直接或间接地影响着土壤中养分的有效性、微生物的活性以及农作物的生长发育。因…

Trimble三维激光扫描-地下公共设施维护的新途径【沪敖3D】

三维激光扫描技术生成了复杂隧道网络的高度详细的三维模型 项目背景 纽约州北部的地下通道网络已有100年历史&#xff0c;其中包含供暖系统、电线和其他公用设施&#xff0c;现在已经开始显露出老化迹象。由于安全原因&#xff0c;第三方的进入受到限制&#xff0c;在没有现成纸…

开发环境搭建-1:配置 WSL (类 centos 的 oracle linux 官方镜像)

一些 Linux 基本概念 个人理解&#xff0c;并且为了便于理解&#xff0c;可能会存在一些问题&#xff0c;如果有根本上的错误希望大家及时指出 发行版 WSL 的系统是基于特定发行版的特定版本的 Linux 发行版 有固定组织维护的、开箱就能用的 Linux 发行版由固定的团队、社…

从零到上线:Node.js 项目的完整部署流程(包含 Docker 和 CICD)

从零到上线&#xff1a;Node.js 项目的完整部署流程&#xff08;包含 Docker 和 CI/CD&#xff09; 目录 项目初始化&#xff1a;构建一个简单的 Node.js 应用设置 Docker 环境&#xff1a;容器化你的应用配置 CI/CD&#xff1a;自动化构建与部署上线前的最后检查&#xff1a;…

安卓动态设置Unity图形API

命令行方式 Unity图像api设置为自动,安卓动态设置Vulkan、OpenGLES Unity设置 安卓设置 创建自定义活动并将其设置为应用程序入口点。 在自定义活动中,覆盖字符串UnityPlayerActivity。updateunitycommandlineararguments (String cmdLine)方法。 在该方法中,将cmdLine…

python如何导出数据到excel文件

python导出数据到excel文件的方法&#xff1a; 1、调用Workbook()对象中的add_sheet()方法 wb xlwt.Workbook() ws wb.add_sheet(A Test Sheet) 2、通过add_sheet()方法中的write()函数将数据写入到excel中&#xff0c;然后使用save()函数保存excel文件 ws.write(0, 0, 1234…

虚幻基础-1:cpu挑选(14600kf)

能帮到你的话&#xff0c;就给个赞吧 &#x1f618; 文章目录 ue非常吃cpu拉满主频打开项目编写蓝图运行原因 时间长 关于压力测试 本文以14600kf为例&#xff0c;双12购入&#xff0c;7月份产。 ue非常吃cpu 经本人测试&#xff0c;ue是非常吃cpu的。 拉满主频 无论任何时间…

MECD+: 视频推理中事件级因果图推理--VLM长视频因果推理

论文链接&#xff1a;https://arxiv.org/pdf/2501.07227v1 1. 摘要及主要贡献点 摘要&#xff1a; 视频因果推理旨在从因果角度对视频内容进行高层次的理解。然而&#xff0c;目前的研究存在局限性&#xff0c;主要表现为以问答范式执行&#xff0c;关注包含孤立事件和基本因…