前面我们已经学习写了一个简单地定长内存池。并且设计出来了高性能并发内存池的框架,接下来我们就来写代码
本期相关的代码已经上传到作者的个人gitee:高并发内存池: 个人学习的项目------高并发内存池喜欢请点个赞谢谢
目录
Common
共同需要的代码
cpp
#pragma once
#include"MemoryAllocator.h"
#include <iostream>
#include <vector>
#include<algorithm>
#include <cstdlib>
#include<stdexcept>
#include<cassert>
#include<thread>
#include<mutex>
#include<memory>
#include <stddef.h> // 跨平台定义 size_t,必须包含
#ifdef _WIN32
// Windows 平台:VirtualAlloc 需要 windows.h
// 同时定义 NOMINMAX 避免与 std::min 冲突(若后续使用)
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#else
// Linux 平台:mmap、sysconf 需要以下头文件
#include <sys/mman.h> // mmap, MAP_FAILED, PROT_READ, PROT_WRITE, MAP_PRIVATE, MAP_ANONYMOUS
#include <unistd.h> // sysconf, _SC_PAGESIZE
#endif
using std::cout;
using std::endl;
using std::vector;
//内存池可申请的最大的内存------256KB
static const int MAX_SIZE = 256 * 1024;
static const int NFRESSLISTS = 208;
static const size_t NPAGES = 129;
static const size_t PAGE_SHIFT = 13;
// 跨平台类型定义:PageID_
// 适用:Windows (32/64位) + Linux (32/64位)
// 1. 64 位 Windows 系统
#if defined(_WIN64)
typedef unsigned long long PageID;
// 2. 32 位 Windows 系统
#elif defined(_WIN32)
typedef size_t PageID;
// 3. Linux 系统(自动适配 32/64 位)
#elif defined(__linux__)
typedef size_t PageID;
// 4. 不支持的平台(报错提示)
#else
#error "当前仅支持 Windows 和 Linux 系统!"
#endif
//给一个对象取前4/8字节
static inline void*& NextObject(void* object)
{
return *(void**)object;
}
//管理好切分的小对象的链表
class FreeList
{
private:
void* freelist_=nullptr;
size_t maxSize_=1;
size_t size_ = 0;
public:
void Push(void* object)
{
//头插
/*if (object == nullptr)
{
throw "申请的对象内存为空";
}*/
assert(object);
NextObject(object) = freelist_;
freelist_ = object;
++size_;
}
void PushRange(void*start,void* end,size_t n)
{
NextObject(start) = freelist_;
freelist_ = start;
size_ += n;
}
void* Pop()
{
//头删
/*if (freelist_ == nullptr)
{
throw "内存链表为空";
}*/
assert(freelist_);
void* object = freelist_;
freelist_ = NextObject(object);
--size_;
return object;
}
void PopRange(void*& start, void*& end, size_t n)
{
assert(n>=size_);
start = freelist_;
end = start;
for (int i = 0; i < n - 1; ++i)
{
end = NextObject(end);
}
freelist_ = NextObject(end);
NextObject(end) = nullptr;
size_ -= n;
}
//判断是否为空
bool Empty()
{
return freelist_ == nullptr;
}
size_t& MaxSize()
{
return maxSize_;
}
size_t size()
{
return size_;
}
};
//对齐映射规则
//以8字节对齐最合适------因为64系统下一个指针是8个字节,导致无法储存指针进而挂接在链表上
class Alignment
{
// 整体控制在最多10%左右的内碎片浪费
// [1,128] 8byte对齐 freelist[0,16)
// [128+1,1024] 16byte对齐 freelist[16,72)
// [1024+1,8*1024] 128byte对齐 freelist[72,128)
// [8*1024+1,64*1024] 1024byte对齐 freelist[128,184)
// [64*1024+1,256*1024] 8*1024byte对齐 freelist[184,208)
private:
/*size_t _RoundUP(size_t size, size_t Align)
{
//方式一:普通运算
size_t alignsize = size;
if (size %8!=0)
{
return (size / Align + 1) * Align;
}
else
{
return alignsize;
}
return alignsize;
}*/
//方式二:位运算
//static防止多次定义 constexpr编译期计算 inline建议编译器编译期内联
static constexpr inline size_t _RoundUP(size_t size, size_t Align)
{
return (size + Align - 1) & ~(Align - 1);
//比如size=5,则二进制为00000101,Align为8,则Align - 1为00000111
// size + Align - 1为00001100,即12
//~为按位取反, ~(Align - 1)为11111000
//&为按位与,当对应位数均为1时方为1
//00001100&11111000=>
//00001100
//11111000
//=>00001000,为8
}
//配合Index的子函数
/*static inline size_t _Index(size_t bytes, size_t align_shift)
{
return ((bytes + (1 << align_shift) - 1) >> align_shift) - 1;
}*/
public:
//频繁调用的小函数常见写法
static constexpr inline size_t RoundUP(size_t size)
{
if (size <= 128)//128B
{
return _RoundUP(size, 8);
}
else if (size < 1024)//1KB
{
return _RoundUP(size, 16);
}
else if (size <= 8 * 1024)//8KB
{
return _RoundUP(size, 128);
}
else if (size <= 64 * 1024)//64KB
{
return _RoundUP(size, 1024);
}
else if (size <= 256 * 1024)//256KB
{
return _RoundUP(size, 8 * 1024);
}
else
{
throw std::runtime_error("申请内存过大");
return 0;
}
}
//计算映射到哪个桶内的自由链表
//这里除法计算的都是2的幂数,现代主流编译器(MSVC、GCC、Clang会将其转化为位运算)
static constexpr inline size_t Index(size_t size)
{
size_t aligned = RoundUP(size); // 先对齐,可能抛出异常
if (aligned <= 128) // 8字节对齐区间 [8, 128]
{
// 桶索引:8→0, 16→1, ..., 128→15
return aligned / 8 - 1;
}
else if (aligned <= 1024) // 16字节对齐区间 [144, 1024]
{
// 起始144对应桶16,步长16
return (aligned - 144) / 16 + 16;
}
else if (aligned <= 8 * 1024) // 128字节对齐区间 [1152, 8192]
{
// 起始1152对应桶72,步长128
return (aligned - 1152) / 128 + 72;
}
else if (aligned <= 64 * 1024) // 1024字节对齐区间 [9216, 65536]
{
// 起始9216对应桶128,步长1024
return (aligned - 9216) / 1024 + 128;
}
else if (aligned <= 256 * 1024) // 8*1024字节对齐区间 [73728, 262144]
{
// 起始73728对应桶184,步长8192
return (aligned - 73728) / (8 * 1024) + 184;
}
else // 理论上不会到达这里,因为 RoundUP 已经对过大 size 抛异常
{
throw std::runtime_error("申请内存过大");
}
}
//第一个版本,可能效率更高,但是可读性略差
//static constexpr inline size_t Index(size_t size)
//{
// size_t aligned = RoundUP(size);
// if (aligned <= 128)
// return _Index(aligned, 3); // 8字节对齐
// else if (aligned <= 1024)
// return _Index(aligned - 128, 4) + 16; // 16字节对齐
// else if (aligned <= 8 * 1024)
// return _Index(aligned - 1024, 7) + 72; // 128字节对齐
// else if (aligned <= 64 * 1024)
// return _Index(aligned - 8 * 1024, 10) + 128; // 1024字节对齐
// else if (aligned <= 256 * 1024)
// return _Index(aligned - 64 * 1024, 13) + 184; // 8192字节对齐
// else
// throw std::runtime_error("申请内存过大");
//}
// 一次thread cache从中心缓存获取多少个
static size_t NumMoveSize(size_t size)
{
assert(size >0);
// [2, 512], 一次批量移动多少个对象的(慢启动)上限值
// 小对象一次批量上限高
// 小对象一次批量上限低
int num = MAX_SIZE / size;
if (num < 2)
num = 2;
if (num > 512)
num = 512;
return num;
}
// 计算一次向系统获取几个页
// 单个对象 8byte
// ...
// 单个对象 256KB
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和PageCache都需要调用
struct Span
{
PageID PageNum=0; //页数
size_t n_=0; //多个大块内存的起始页的页号
//双向链表
Span* prev_=nullptr;
Span* next_=nullptr;
void* freeList_=nullptr;//切出来小内存的自由链表
size_t IsUseCount_=0; //被切出去的内存,分配给ThreadCache计数
bool IsUse = false; //是否被使用
size_t ObjectSize=0; //对象数量
};
//带头双向循环链表
class SpanList
{
private:
std::unique_ptr<Span> head_; // 头节点由 unique_ptr 独占管理
//多线程访问同一个桶会形成竞争导致变慢,访问各自的桶即使是加锁也不会变慢
public:
std::mutex mtx_;
SpanList()
: head_(std::make_unique<Span>())
{
head_->prev_ = head_.get();
head_->next_ = head_.get();
}
Span* Begin()
{
return head_->next_;
}
Span* End()
{
return head_->prev_;
}
void PushFront(Span* span)
{
Insert(Begin(), span);
}
Span* PopFront()
{
Span* target = head_->next_;
Erase(target);
return target;
}
bool Empty()
{
return head_->next_ == head_.get();
}
// 插入:将 newSpan 插入到 pos 之前(pos 不能为 nullptr)
void Insert(Span* pos, Span* newSpan)
{
assert(pos);
assert(newSpan);
// 连接前后节点
Span* prev = pos->prev_;
prev->next_ = newSpan;
newSpan->prev_ = prev;
newSpan->next_ = pos;
pos->prev_ = newSpan;
// newSpan 的所有权已转移给链表(由链表负责析构时释放)
}
// 删除:从链表中移除 pos 指向的节点(不能是头节点)
void Erase(Span* pos)
{
assert(pos);
// 确保不删除头节点
if (pos == head_.get())
{
return; // 头节点由 unique_ptr 管理,不能删除
}
// 从链表中摘除
pos->prev_->next_ = pos->next_;
pos->next_->prev_ = pos->prev_;
// 重置指针,避免悬空指针
pos->prev_ = nullptr;
pos->next_ = nullptr;
}
// 析构函数:释放所有非头节点
~SpanList()
{
Span* cur = head_->next_;
while (cur != head_.get())
{
Span* next = cur->next_;
delete cur; // 释放每个普通节点
cur = next;
}
// head_ 由 unique_ptr 自动释放,无需手动处理
}
};
ThreadCache
设计
我们之前设计的定长内存池应对单线程没有问题。但是对于多线程需要不同内存的资源来说,这样显然是不够的。我们也可以这样设计

针对于8kb以下的可以申请8kb内存,针对16kb以下8kb以上的申请16kb内存
但是这样会导致内存资源浪费。申请的一大块内存因为内存对齐的需求,多余的没有利用内存,导致内存碎片问题,这就是内碎片。而外碎片是这段空间被切成碎片,部分还回来了但是并不连续,导致了内存碎片问题。
为了解决这个问题,我们就设计了ThreadCache
ThreadCache 是一个哈希桶,需要申请的内存通过计算来映射其所在的是哪个桶,每个桶内都拥有一个按桶位置映射大小的内存块对象的自由链表。每个线程都会有一个thread cache对象,这样每个线程在这里获取对象和释放对象时是无锁的
Common.h
cpp
#pragma once
#include <iostream>
#include <vector>
#include <cstdlib>
#include<stdexcept>
#include<cassert>
using std::cout;
using std::endl;
using std::vector;
//内存池可申请的最大的内存------256KB
static const int MAX_SIZE=256 * 1024;
static const int NFRESSLISTS = 208;
//给一个对象取前4/8字节
void*& NextObject(void* object)
{
return *(void**)object;
}
//管理好切分的小对象的链表
class FreeList
{
private:
void* freelist_;
public:
void Push(void* object)
{
//头插
if (object == nullptr)
{
throw "申请的对象内存为空";
}
NextObject(object) = freelist_;
freelist_ = object;
}
void* Pop()
{
//头删
if (freelist_ == nullptr)
{
throw "内存链表为空";
}
void* object = freelist_;
freelist_ = NextObject(object);
}
//判断是否为空
bool Empty()
{
return freelist_ == nullptr;
}
};
ThreadCache.h
cpp
#pragma once
#include"Common.h"
class ThreadCache
{
public:
//申请释放资源
void *Allocator(size_t size)
{
}
void Deallocator(size_t size ,void*ptr)
{
}
private:
};
内存对齐规则
我们将以以下这种方式映射
1,128\] 8byte对齐 freelist\[0,16) \[128+1,1024\] 16byte对齐 freelist\[16,72) \[1024+1,8\*1024\] 128byte对齐 freelist\[72,128) \[8\*1024+1,64\*1024\] 1024byte对齐 freelist\[128,184) \[64\*1024+1,256\*1024\] 8\*1024byte对齐 freelist\[184,208)
因此我们最大的自由链表桶数为208
cpp
//最大的自由链表桶数
static const int NFRESSLISTS = 208;
这样就可以改ThreadCache.h
cpp
#pragma once
#include"Common.h"
class ThreadCache
{
public:
//申请释放资源
void* Allocator(size_t size);
void Deallocator(size_t size, void* ptr);
private:
FreeList freelist_[NFRESSLISTS];
};
尽管这个数字仍然不能避免内存碎片和浪费,但是已经做出了取舍。如果桶太少(比如只分几十个),则每个桶覆盖的范围过大,内部碎片会增加;如果桶太多(比如上千个),则管理数组本身的内存开销增大,且查找桶时可能需更多时间
类似的,在谷歌的项目------tcmalloc中桶的数量大约也是200多个,这是一个经历了工程考研的经验数值。
内存映射以8字节对齐最合适------因为64系统下一个指针是8个字节,导致无法储存指针进而挂接在链表上
我们可能写出初版:
cpp
//对齐映射规则
//以8字节对齐最合适------因为64系统下一个指针是8个字节,导致无法储存指针进而挂接在链表上
class Alignment
{
private:
size_t _RoundUP(size_t size, size_t Align)
{
//方式一:普通运算
size_t alignsize = size;
if (size %8!=0)
{
return (size / Align + 1) * Align;
}
else
{
return alignsize;
}
return alignsize;
}
public:
//频繁调用的小函数常见写法
static constexpr inline size_t RoundUP(size_t size)
{
if (size <= 128)//128B
{
return _RoundUP(size,8);
}
else if (size<1024)//1KB
{
return _RoundUP(size, 16);
}
else if (size<=8*1024)//8KB
{
return _RoundUP(size, 128);
}
else if (size<=64*1024)//64KB
{
return _RoundUP(size, 1024);
}
else if (size<=256*1024)//256KB
{
return _RoundUP(size, 8*1024);
}
else
{
throw std::runtime_error("申请内存过大");
return 0;
}
}
但是这种取模、除法运算对于CPU来说比较慢,还有更快的方式:
cpp
//方式二:位运算
//static防止多次定义 constexpr编译期计算 inline建议编译器编译期内联
static constexpr inline size_t _RoundUP(size_t size, size_t Align)
{
return (size + Align - 1) & ~(Align - 1);
}
以上这个写法第一眼可能比较懵,没关系我们来以一个例子解释:
当size为5时,则二进制为00000101;Align为8,则Align - 1为00000111
size + Align - 1为00001100,即12
~为按位取反 , ~(Align - 1)为11111000
&为按位与,当对应位数均为1时方为1
因此,(size + Align - 1) & ~(Align - 1)=>
00001100&11111000
=>00001100
11111000
=>00001000
结果为8
接下来我们还要计算申请到的内存被挂载到哪个桶上,代码如下
cpp
//计算映射到哪个桶内的自由链表
//这里除法计算的都是2的幂数,现代主流编译器(MSVC、GCC、Clang会将其转化为位运算)
static constexpr inline size_t Index(size_t size)
{
size_t aligned = RoundUP(size); // 先对齐,可能抛出异常
if (aligned <= 128) // 8字节对齐区间 [8, 128]
{
// 桶索引:8→0, 16→1, ..., 128→15
return aligned / 8 - 1;
}
else if (aligned <= 1024) // 16字节对齐区间 [144, 1024]
{
// 起始144对应桶16,步长16
return (aligned - 144) / 16 + 16;
}
else if (aligned <= 8 * 1024) // 128字节对齐区间 [1152, 8192]
{
// 起始1152对应桶72,步长128
return (aligned - 1152) / 128 + 72;
}
else if (aligned <= 64 * 1024) // 1024字节对齐区间 [9216, 65536]
{
// 起始9216对应桶128,步长1024
return (aligned - 9216) / 1024 + 128;
}
else if (aligned <= 256 * 1024) // 8*1024字节对齐区间 [73728, 262144]
{
// 起始73728对应桶184,步长8192
return (aligned - 73728) / (8 * 1024) + 184;
}
else // 理论上不会到达这里,因为 RoundUP 已经对过大 size 抛异常
{
throw std::runtime_error("申请内存过大");
}
}
这里不需要进行位运算,因为现代编译器对2的幂的除法编译的过程中会自行转化为位运算,实际效果并不会比显式写位运算差
接着我们就可以写ThreadCache内存池申请资源了
cpp
void*ThreadCache::Allocator(size_t size)
{
assert(size <= MAX_SIZE);
//对申请的内存对齐
size_t AlignSize = Alignment::RoundUP(size);
//计算映射的桶
size_t index = Alignment::Index(size);
if (!freelist_[index].Empty())//链表内存不为空
return freelist_[index].Pop();
else//链表内存为空,则自行申请内存
FetchFromCentralCache(index, AlignSize);
}
TLS无锁线程访问
一个进程有多个线程,如果每个线程都需要访问,并发条件下,用锁会导致串行,这样在高并发下很有可能效率更低,还会有CPU上下文转换的性能开销。这样我们就不想要锁,那么没有锁的化,我们该怎么办呢?
我们可以利用TLS技术------Thread Local Storage,即线程本地存储。
Thread Local Storage(TLS,线程本地存储)是一种线程隔离的存储机制 ,它允许为每个线程创建并维护变量的独立副本,确保线程之间的数据互不干扰。核心机制:TLS的本质是为每个线程分配一块独立的内存空间,用于存储该线程专属的变量实例。当线程访问这些变量时,会自动访问自己的副本,而不会影响其他线程的同名变量。
在C++11后,拥有单独的关键字实现
cpp
thread_local int thread_id; // 每个线程有独立的thread_id副本
而历史角度上,也有两种非跨平台的实现方式:
Windows :通过 TlsAlloc、TlsSetValue、TlsGetValue 等 API 分配和使用 TLS 槽,编译器支持 __declspec(thread) 语法。
Linux/Unix :早期用 __thread 关键字(GCC扩展)
TLS的优缺点
优点
-
线程安全:变量副本独立,无需互斥锁,避免了线程竞争和死锁风险。
-
性能提升:无锁操作减少了同步开销,适合高频访问的场景。
-
简化代码:无需手动管理线程间的数据隔离,代码更简洁。
缺点
-
内存开销:每个线程都有变量副本,可能增加内存使用(特别是线程数量多时)。
-
初始化开销:线程首次访问TLS变量时需要初始化,可能影响首次访问性能。
-
生命周期管理:TLS变量的构造和析构时机与线程生命周期相关,需注意资源释放。
源代码
TLSManager.h
cpp
#pragma once
#include "ThreadCache.h"
#include <memory>
/**
* @brief TLS生命周期管理器,使用RAII模式自动管理ThreadCache实例
* @details 每个线程拥有独立的ThreadCache实例,线程结束时自动清理资源
*/
class TLSManager
{
public:
/**
* @brief 获取当前线程的ThreadCache实例
* @return ThreadCache& 当前线程的ThreadCache引用
* @note 线程首次访问时自动创建实例,线程结束时自动销毁
*/
static ThreadCache& GetInstance()
{
if (!instance_)
{
instance_ = std::make_unique<ThreadCache>();
}
return *instance_;
}
/**
* @brief 清理当前线程的ThreadCache资源
* @note 通常不需要手动调用,线程结束时会自动调用
*/
static void Cleanup()
{
instance_.reset();
}
/**
* @brief 检查当前线程是否有ThreadCache实例
* @return bool 如果有实例返回true,否则false
*/
static bool HasInstance()
{
return instance_ != nullptr;
}
private:
// 每个线程独立的ThreadCache实例
static thread_local std::unique_ptr<ThreadCache> instance_;
};
// 定义thread_local变量
thread_local std::unique_ptr<ThreadCache> TLSManager::instance_ = nullptr;
ConcurrentAlloc.h
cpp
#pragma once
#include"Common.h"
#include"ThreadCache.h"
#include"TLSManager.h"
//并发申请释放内存
static void* ConcurrrentAlloc(size_t size)
{
//通过TLSManager获取当前线程的ThreadCache实例
//RAII模式自动管理生命周期,线程结束时自动清理
return TLSManager::GetInstance().Allocator(size);
}
static void ConcurrrentDealloc(void* ptr, size_t size)
{
//通过TLSManager获取当前线程的ThreadCache实例
TLSManager::GetInstance().Deallocator(ptr, size);
}
ThreadCache.h
cpp
#pragma once
#include "Common.h"
class ThreadCache
{
public:
//申请释放资源
void* Allocator(size_t size);
void Deallocator(void* ptr, size_t size);
//从中心缓存获取
void* FetchFromCentralCache(size_t index, size_t size);
// 释放对象时,链表过长时,回收内存回到中心缓存
void ListTooLong(FreeList& list, size_t size);
private:
FreeList freelist_[NFRESSLISTS];
};
// 声明为 extern thread_local,全局只有一个 TLS 变量
extern thread_local ThreadCache* pTLSThreadCache;
ThreadCache.cpp
cpp
#define _CRT_SECURE_NO_WARNINGS
#include "ThreadCache.h"
#include"CentralCache.h"
// 跨平台min函数包装器,避免Windows.h的宏冲突
namespace PlatformUtils
{
template<typename T>
inline const T& Min(const T& a, const T& b)
{
return (a < b) ? a : b;
}
}
// 定义 thread_local 变量(每个线程独立)
thread_local ThreadCache* pTLSThreadCache = nullptr;
//申请资源
void* ThreadCache::Allocator(size_t size)
{
assert(size <= MAX_SIZE);
size_t AlignSize = Alignment::RoundUP(size);
size_t index = Alignment::Index(size);
if (!freelist_[index].Empty())
return freelist_[index].Pop();
else
return FetchFromCentralCache(index, AlignSize);
}
//释放资源
void ThreadCache::Deallocator(void* ptr, size_t size)
{
assert(ptr);
assert(size <= MAX_SIZE);
size_t index = Alignment::Index(size);
freelist_[index].Push(ptr);
//链表长度大于一次批量申请的内存
if (freelist_[index].size() >= freelist_[index].MaxSize())
{
ListTooLong(freelist_[index], size);
}
}
//从CentralCache申请资源
void* ThreadCache::FetchFromCentralCache(size_t index, size_t size)
{
//满开始反馈调节算法
// 1、最开始不会一次向central cache一次批量要太多,因为要太多了可能用不完
// 2、如果你不要这个size大小内存需求,那么batchNum就会不断增长,直到上限
// 3、size越大,一次要的BatchNum越小
// 4、size越小,一次要得BatchNum越大
size_t BatchNum = PlatformUtils::Min(freelist_[index].MaxSize(), Alignment::NumMoveSize(size));
void* start = nullptr;
void* end = nullptr;
size_t actualNum = CentralCache::Instance()->FetchRangeObj(start, end, BatchNum, size);
assert(actualNum > 0);
if (actualNum == 1)
{
assert(start == end);
return start;
}
else
{
freelist_[index].PushRange(NextObject(start), end, actualNum-1);
return start;
}
if (BatchNum == freelist_[index].MaxSize())
{
freelist_[index].MaxSize() += 1;
}
return nullptr;
}
void ThreadCache::ListTooLong(FreeList& list, size_t size)
{
void* start = nullptr;
void* end = nullptr;
list.PopRange(start, end, list.MaxSize());
CentralCache::Instance()->ReleaseListToSpans(start, size);
}
CentralCache
设计
当ThreadCache没有内存的时候,就回去CentralCache申请内存。CentralCache也是一个哈希桶结构,不同的是每个哈希桶位置挂是SpanList链表结构,不过每个映射桶下面的span中的大内存块被按映射关系切成了一个个小内存块对象挂在span的自由链表中。

源代码
CentralCache.h
cpp
#pragma once
#include"Common.h"
//单例模式
class CentralCache
{
private:
// 将指针数组改为对象数组,避免未初始化指针和成员访问错误
SpanList spanlist_[NFRESSLISTS];
static CentralCache sInst_;//只声明不定义
CentralCache() = default;//不希望别人创建对象
CentralCache(const CentralCache&) = delete;//禁止拷贝构造
public:
static CentralCache* Instance()
{
return &sInst_;
}
// 从中心缓存获取一定数量的对象给thread cache
size_t FetchRangeObj(void*& start, void*& end, size_t n, size_t byte_size);
Span* GetOneSpan(SpanList& list, size_t size);
// 将一定数量的对象释放到span跨度
void ReleaseListToSpans(void* start, size_t byte_size);
};
CentralCache.cpp
cpp
#define _CRT_SECURE_NO_WARNINGS
#include"CentralCache.h"
#include"PageCache.h"
CentralCache CentralCache::sInst_;
// 获取一个非空的span
Span* CentralCache::GetOneSpan(SpanList& list, size_t size)
{
//查看当前是否有没有被分配的span
Span* it = list.Begin();
while (it != list.End())
{
if (it->freeList_ != nullptr)
{
return it;
}
else
{
it = it->next_;
}
}
//先解除CentralCache的桶锁,这样其他线程释放资源的时候就不会阻塞
list.mtx_.unlock();
//走到这里证明没有没有被分配的span,只能去PageCache取申请
PageCache::Instance()->pagemtx_.lock();
Span*span=PageCache::Instance()->GetSpan(Alignment::NumMovePage(size));
span->IsUse = true;
PageCache::Instance()->pagemtx_.unlock();
//不需要加锁,因为这样可能会导致切分后其他线程无法访问该span
//计算span的起始地址
char* start = (char*)(span->PageNum << PAGE_SHIFT);
size_t bytesize = span->n_<< PAGE_SHIFT;//得知页号
char* end = start + bytesize;
//span切为小块挂在自由链表上挂起
span->freeList_ = start;
start += size;
void* tail = span->freeList_;
while (start < end)
{
NextObject(tail)=start;
tail = NextObject(tail);
start += size;
}
//切好后挂到桶内再加锁
list.mtx_.lock();
list.PushFront(span);
return span;
}
// 从中心缓存获取一定数量的对象给thread cache
size_t CentralCache::FetchRangeObj(void*& start, void*& end, size_t batchNum, size_t size)
{
size_t index = Alignment::Index(size);
spanlist_[index].mtx_.lock();
Span* span = GetOneSpan(spanlist_[index],size);
assert(span);
assert(span->freeList_);
start = span->freeList_;
end = start;
size_t i = 0;
size_t actualNum = 1;
while (i< batchNum -1&&NextObject(end)!=nullptr)
{
end = NextObject(end);
++i;
}
span->freeList_ = NextObject(end);
NextObject(end) = nullptr;
spanlist_[index].mtx_.unlock();
return actualNum;
}
void CentralCache::ReleaseListToSpans(void* start, size_t byte_size)
{
size_t index = Alignment::Index(byte_size);
spanlist_[index].mtx_.lock();
while (start)
{
void* next = NextObject(start);
Span* span = PageCache::Instance()->MapObjectToSpan(start);
NextObject(start)=span->freeList_;
span->freeList_ = start;
span->IsUseCount_--;
// 说明span的切分出去的所有小块内存都回来了
// 这个span就可以再回收给page cache,pagecache可以再尝试去做前后页的合并
if (span->IsUseCount_ == 0)
{
spanlist_[index].Erase(span);
span->freeList_ = nullptr;
span->next_ = nullptr;
span->prev_ = nullptr;
// 释放span给page cache时,使用page cache的锁就可以了
// 这时把桶锁解开掉
spanlist_[index].mtx_.unlock();
PageCache::Instance()->pagemtx_.lock();
PageCache::Instance()->ReleaseSpanToPageCache(span);
PageCache::Instance()->pagemtx_.unlock();
spanlist_[index].mtx_.lock();
}
start = next;
}
spanlist_[index].mtx_.unlock();
}
PageCache
设计
申请内存:
-
当central cache向page cache申请内存时,page cache先检查对应位置有没有span,如果没有则向更大页寻找一个span,如果找到则分裂成两个。比如:申请的是4页page,4页page后面没有挂span,则向后面寻找更大的span,假设在10页page位置找到一个span,则将10页page span分裂为一个4页page span和一个6页page span。
-
如果找到_spanList[128]都没有合适的span,则向系统使用mmap、brk或者是VirtualAlloc等方式申请128页page span挂在自由链表中,再重复1中的过程。
-
需要注意的是central cache和page cache 的核心结构都是spanlist的哈希桶,但是他们是有本质区别的,central cache中哈希桶,是按跟thread cache一样的大小对齐关系映射的,他的spanlist中挂的span中的内存都被按映射关系切好链接成小块内存的自由链表。而page cache 中的spanlist则是按下标桶号映射的,也就是说第i号桶中挂的span都是i页内存。
释放内存: -
如果central cache释放回一个span,则依次寻找span的前后page id的没有在使用空闲span,看是否可以合并,如果合并继续向前寻找。这样就可以将切小的内存合并收缩成大的span,减少内存碎片。

不过这里我们就涉及到加锁的问题。

对于一个线程来说,是不需要锁的。因为一个线程申请自己的内存,互相之之间不存在竞争,因此ThreadCache是不需要加锁的。
对于CentralCache来说,不同的线程对应不同的桶的时候是没有竞争的,也就不需要上锁,只有当多个线程竞争同一个锁的时候才会竞争,因此只需要对桶加锁就够了。
对于PageCaChe来说,需要整个锁。但是不能单纯用互斥锁,因为会死锁。我们可以采用两种方式解决:
1、用recursive_mutex。递归进行锁的时候会进行检查防止死锁。
2、分离子函数和主函数。子函数实现业务逻辑,主函数加锁控制
但是我们可以这样在CentralCache上加锁(见上面的源码)
源代码
PageCache.h
cpp
#pragma once
#include"Common.h"
#include<unordered_map>
class PageCache
{
private:
SpanList spanlist_[NFRESSLISTS];
std::unordered_map<PageID, Span*>IdSpanMap_;
static PageCache sInstan_;
PageCache() = default;
PageCache(const PageCache&) = delete;
public:
static PageCache* Instance()
{
return &sInstan_;
}
// 获取从对象到span的映射
Span* MapObjectToSpan(void* obj);
// 释放空闲span回到Pagecache,并合并相邻的span
void ReleaseSpanToPageCache(Span* span);
std::mutex pagemtx_;
//获取K页的span
Span* GetSpan(size_t K);
};
PageCache.cpp
cpp
#define _CRT_SECURE_NO_WARNINGS
#include"PageCache.h"
PageCache PageCache::sInstan_;
Span* PageCache::MapObjectToSpan(void* object)
{
PageID id = ((PageID)object >> PAGE_SHIFT);
auto ret = IdSpanMap_.find(id);
if (ret != IdSpanMap_.end())
{
return ret->second;
}
else
{
assert(false);
return nullptr;
}
}
Span* PageCache::GetSpan(size_t K)
{
assert(K > 0 && K < NPAGES);
if (!spanlist_[K].Empty())
{
return spanlist_[K].PopFront();
}
// 检查一下后面的桶里面有没有span,如果有可以把他它进行切分
for (size_t i = K + 1; i < NPAGES; ++i)
{
if (!spanlist_[i].Empty())
{
Span* nSpan = spanlist_[i].PopFront();
Span* kSpan = new Span;
// 在nSpan的头部切一个k页下来
// k页span返回
// nSpan再挂到对应映射的位置
kSpan->PageNum = nSpan->PageNum;
kSpan->n_ = K;
nSpan->PageNum += K;
nSpan->n_ -= K;
spanlist_[nSpan->n_].PushFront(nSpan);
//存储span页号与span的映射,方便回收时合并查找
IdSpanMap_[nSpan->PageNum] = nSpan;
// 1000 5
IdSpanMap_[nSpan->PageNum + nSpan->n_ - 1] = nSpan;
//建立ID与span映射,方便CentralCache回收时查找对应的span
for (PageID i = 0; i < kSpan->n_; ++i)
{
IdSpanMap_[kSpan->PageNum + i] = kSpan;
}
return kSpan;
}
}
// 走到这个位置就说明后面没有大页的span了
// 这时就去找堆要一个128页的span
Span* bigSpan = new Span;
void* ptr = SystemAlloc(NPAGES - 1);
bigSpan->PageNum = (PageID)ptr >> PAGE_SHIFT;
bigSpan->n_ = NPAGES - 1;
spanlist_[bigSpan->n_].PushFront(bigSpan);
return GetSpan(K);
}
void PageCache::ReleaseSpanToPageCache(Span* span)
{
// 对span前后的页,尝试进行合并,缓解内存碎片问题
//向前合并
while (1)
{
PageID prevId = span->PageNum - 1;
auto ret = IdSpanMap_.find(prevId);
// 前面的页号没有,不合并了
if (ret == IdSpanMap_.end())
{
break;
}
// 前面相邻页的span在使用,不合并了
Span* prevSpan = ret->second;
if (prevSpan->IsUse == true)
{
break;
}
// 合并出超过128页的span没办法管理,不合并了
if (prevSpan->n_ + span->n_ > NPAGES - 1)
{
break;
}
span->PageNum = prevSpan->PageNum;
span->n_ += prevSpan->n_;
spanlist_[prevSpan->n_].Erase(prevSpan);
delete prevSpan;
}
// 向后合并
while (1)
{
PageID nextId = span->PageNum + 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_;
spanlist_[nextSpan->n_].Erase(nextSpan);
delete nextSpan;
}
spanlist_[span->n_].PushFront(span);
span->IsUse = false;
IdSpanMap_[span->PageNum] = span;
IdSpanMap_[span->PageNum + span->n_ - 1] = span;
}
本篇内容到这里就结束了。内存池的三大Cache已经完成,后续我们还会对其进行调优测试。敬请期待
封面图自取:
