目录
[3. ConCurrentAlloc.h](#3. ConCurrentAlloc.h)
[4. ThreadCache.h](#4. ThreadCache.h)
前言:
前面一个设计了一个定长内存池,经过与传统的内存申请方式对比,初步实现了一个可承受频繁申请与释放的高性能的内存池,这是我们首先要看到的一个内存池被设计出来实现的可观效果。从这里开始,我们将一步步的对高并发内存池进行由浅入深的设计,本节我们来设计ThreadCache。
threadcache:线程缓存是每个线程独有的,⽤于小于256KB的内存的分配,线程从这里申请内存 不需要加锁,每个线程独享⼀个cache,这也就是这个并发线程池高效的地方

1.Common.hpp
我们将公共代码独立合并到这个文件当中,方便调用
cpp
#pragma once
#include <iostream>
#include <vector>
#include <ctime>
#include <new>
#include <assert.h>
#include <thread>
static const size_t MAX_SIZE = 256 * 1024;
static const size_t NFREE_LIST = 208;
static void*& Next(void* obj)
{
return *(void**)obj;
}
class FreeList
{
public:
void Push(void* obj)
{
Next(obj) = _freelist_head;
_freelist_head = obj;
}
void* Pop()
{
void* obj = _freelist_head;
_freelist_head = Next(obj);
return obj;
}
bool Empty()
{
return _freelist_head == nullptr;
}
private:
void* _freelist_head;
};
class SizeClass
{
public:
// 整体控制在最多10%的浪费
// [1,128] 8 byte对齐 freelist[0,16) //16个桶
// [128+1,1024] 16byte 对齐 freelist[16,72)
// [1024+1,8*1024] 128 byte对齐 freelist[72,128)
// [8*1024+1,64*1024] 1024 byte对齐 freelist[128,184)
// [64*1024+1,256*1024] 8*1024 byte对齐 freelist[184,208)
static inline size_t _RoundUp(size_t size, size_t alignNum)
{
return ((size + alignNum - 1) & ~(alignNum - 1));
}
//向上对齐 7->8 8->8 9->16
static inline size_t RoundUp(size_t size)
{
if (size <= 128)
{
return _RoundUp(size, 8);
}
else if (size < 1024)
{
return _RoundUp(size, 16);
}
else if (size < 8 * 1024)
{
return _RoundUp(size, 128);
}
else if (size < 64 * 1024)
{
return _RoundUp(size, 1024);
}
else if (size < 256 * 1024)
{
return _RoundUp(size, 8 * 1024);
}
else {
assert(false);
return -1;
}
}
// 前16个桶每个桶挂一个链,链上的每一块内存大小都一样,往后每个桶按照8字节的步长增加
// 第一个桶的内存都是8字节,第二个都是16字节...最后一个都是128字节
static inline size_t _Index(size_t bytes, size_t align_shift)
{
return ((bytes + (1 << align_shift) - 1) >> align_shift) - 1;
}
// 计划映射到哪一个自由链表桶
static inline size_t Index(size_t bytes)
{
//每个区间有多少个链
static int group_array[4] = { 16, 56, 56, 56 };
if (bytes <= 128) {
return _Index(bytes, 3);
}
else if (bytes <= 1024) {
return _Index(bytes - 128, 4) + group_array[0];
}
else if (bytes <= 8 * 1024) {
return _Index(bytes - 1024, 7) + group_array[1] + group_array[0];
}
else if (bytes <= 64 * 1024) {
return _Index(bytes - 8 * 1024, 10) + group_array[2] + group_array[1] + group_array[0];
}
else if (bytes <= 256 * 1024) {
return _Index(bytes - 64 * 1024, 13) + group_array[3] + group_array[2] + group_array[1] + group_array[0];
}
else {
assert(false);
}
return -1;
}
private:
};
2.UnitTest.cpp
我们从用户的角度来一步步探查当前的内存池完成进度
cpp
#include "ObjectPool.hpp"
#include "ConcurrentAlloc.h"
void Alloc1()
{
for (int i = 1; i < 5; i++)
{
void* ptr = ConcurrentAlloc(6);
}
}
void Alloc2()
{
for (int i = 1; i < 5; i++)
{
void* ptr = ConcurrentAlloc(7);
}
}
void TLSTest()
{
std::thread t1(Alloc1);
std::thread t2(Alloc2);
t1.join();
t2.join();
}
int main()
{
//TestObjectPool();
TLSTest();
return 0;
}
这里分别创建了两个线程来执行两个不同的Alloc函数,里面调用的ConcurrentAlloc很明显是为ptr申请内存的,我们来看看这个函数
3. ConCurrentAlloc.h
cpp
#pragma once
#include "ThreadCache.h"
#include "Common.hpp"
static void* ConcurrentAlloc(size_t size)
{
// 通过TLS,每个线程无锁地获取自己的专属ThreadCache对象
if (pTLSThreadCache == nullptr)
{
//懒加载思想,需要用到这个线程的时候再申请该线程的ThreadCache空间
pTLSThreadCache = new ThreadCache;
}
// 向当前线程的ThreadCache中申请内存
return pTLSThreadCache->Allocate(size);
}
// 传size是要确定将不同大小的内存块精确的还给对应桶
static void ConcurrentFree(void* ptr, size_t size)
{
assert(pTLSThreadCache);
pTLSThreadCache->Deallocate(ptr, size);
}
从这里就知道,内存是通过类对象pTLSThreadCache来申请和释放的
4. ThreadCache.h
cpp
#pragma once
#include "Common.hpp"
class ThreadCache
{
public:
void* Allocate(size_t size);
void* Deallocate(void* ptr, size_t size);
void* FetchFromCentralCache(size_t index, size_t alignSize);
private:
// 每个线程都有独自的一批存放内存的链表桶
FreeList _freelists[NFREE_LIST];
};
// TLS Thread Local Storage //线程局部存储,让每一个线程都有独立的存储空间,避免了加锁的开销
static _declspec(thread) ThreadCache* pTLSThreadCache = nullptr;
最后一句代码实现了让每一个线程都拥有一个独立的ThreadCache类,它们各自管理各自内存,互不干扰,因此前面不同线程申请内存的时候并不需要加锁。我们来具体看看ThreadCache类中函数的实现
5.ThreadCache.cpp
cpp
#include "ThreadCache.h"
void* ThreadCache::FetchFromCentralCache(size_t index, size_t alignSize)
{
// ...
return nullptr;
}
void* ThreadCache::Allocate(size_t size)
{
assert(size <= MAX_SIZE);
// 1. 申请字节数,向上对齐
size_t alignSize = SizeClass::RoundUp(size);
// 2. 桶下标
size_t index = SizeClass::Index(size);
if (!_freelists[index].Empty())
{
return _freelists[index].Pop();
}
else {
return FetchFromCentralCache(index, alignSize);
}
}
void* ThreadCache::Deallocate(void* ptr, size_t size)
{
assert(ptr);
assert(size <= MAX_SIZE);
size_t index = SizeClass::Index(size);
_freelists[index].Push(ptr);
}
根据申请的字节数,先向上对齐,然后再根据字节数确定桶下标,也就是从哪个桶申请,如果该桶没有剩余内存了,就需要向中心缓存CentralCache中申请内存,但目前尚未设计到中心缓存结构,因此函数FetchFromCentralCache暂不实现。回收内存机制,需要对目标内存块大小进行判定,确认回收到哪个桶,再Push到该桶中,每个桶都是存放大小一致内存块的链表,该操作就是将内存块头插入链表中。
设计总结:
给每一个线程独立申请一块ThreadCache内存,每一个ThreadCache就是FreeList _freelistsNFREE_LIST,存放208个链表,各个链表之间管理的内存块大小不同,但是同一个链表内的所有内存块大小是一致的。
用户通过不同线程调用ConcurrentAlloc和 ConcurrentFree申请和释放内存,每个线程都有一个独立的ThreadCache类对象pTLSThreadCache,各个线程之间申请内存互不影响,因为pTLSThreadCache是线程独立资源,因此不加锁也可以各自调用Allocate和Deallocate实现并发申请和释放资源,提升内存申请释放效率。
附加理解:
每个线程都有一个独立的ThreadCache对象,也就是pTLSThreadCache那个,但是不是一开始创建线程就申请,如果不用就太浪费了,而是用到了这个线程才会申请出来,这里引入了"懒加载"思想来进行设计。然后根据需要申请的空间,向上对齐 + 桶下标定位之后,向该桶链表申请空间,如果该桶没有空间了。再通过FetchFromCentralCache向中心缓存申请,由此引入中心缓存的结构设计,这个由下一篇详细介绍,目前项目到这里大概就是这么个思路,后续敬请期待~~