【C++高并发内存池】 2.ThreadCache设计
目录
前言:
1.Common.hpp
2.UnitTest.cpp
3. ConCurrentAlloc.h
4. ThreadCache.h
5.ThreadCache.cpp
设计总结:
前言:
前面一个设计了一个定长内存池,经过与传统的内存申请方式对比,初步实现了一个可承受频繁申请与释放的高性能的内存池,这是我们首先要看到的一个内存池被设计出来实现的可观效果。从这里开始,我们将一步步的对高并发内存池进行由浅入深的设计,本节我们来设计ThreadCache。
threadcache:线程缓存是每个线程独有的,⽤于小于256KB的内存的分配,线程从这里申请内存 不需要加锁,每个线程独享⼀个cache,这也就是这个并发线程池高效的地方
1.Common.hpp
我们将公共代码独立合并到这个文件当中,方便调用
#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
我们从用户的角度来一步步探查当前的内存池完成进度
#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
#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
#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
#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 _freelists[NFREE_LIST],存放208个链表,各个链表之间管理的内存块大小不同,但是同一个链表内的所有内存块大小是一致的。
用户通过不同线程调用ConcurrentAlloc和 ConcurrentFree申请和释放内存,每个线程都有一个独立的ThreadCache类对象pTLSThreadCache,各个线程之间申请内存互不影响,因为pTLSThreadCache是线程独立资源,因此不加锁也可以各自调用Allocate和Deallocate实现并发申请和释放资源,提升内存申请释放效率。
附加理解:
每个线程都有一个独立的ThreadCache对象,也就是pTLSThreadCache那个,但是不是一开始创建线程就申请,如果不用就太浪费了,而是用到了这个线程才会申请出来,这里引入了“懒加载”思想来进行设计。然后根据需要申请的空间,向上对齐 + 桶下标定位之后,向该桶链表申请空间,如果该桶没有空间了。再通过FetchFromCentralCache向中心缓存申请,由此引入中心缓存的结构设计,这个由下一篇详细介绍,目前项目到这里大概就是这么个思路,后续敬请期待~~