五、高并发内存池–Thread Cache
5.1 Thread Cache的工作原理
thread cache是哈希桶结构,每个桶是一个按桶位置映射大小的内存块对象的自由链表。每个线程都会有一个thread cache对象,这样每个线程在这里获取对象和释放对象时都是无锁的。
每一个线程都有一个自己的Thread Cache,即每个线程在向自己的Thread Cache申请内存时是线程安全的,无需加锁,线程之间是不会相互产生影响的,所以大大加快了申请内存的效率。但是每一个线程是在什么时候创建这个自己的Thread Cache的呢?又是怎么创建的呢?
在C++中,_declspec(thread)是一个用于指定线程局部存储( Thread Local Storage,TLS)的关键字。通过将该关键字应用于变量声明,可以确保每个线程都拥有其自己的变量实例,而不是共享同一个变量实例。
具体来说,当变量声明为_declspec(thread)时,每个线程都会有一个独立的变量实例,该变量的生命周期与线程的生命周期一致。每个线程对该变量的访问都是线程安全的,不会与其他线程的访问冲突。
使用_declspec(thread)可以在多线程编程中很方便地创建和使用线程局部变量,它通常用于解决多线程环境下的共享数据问题。请注意,_declspec(thread)是Microsoft Visual C++编译器的特定语法,并不在标准的C++语言规范中定义。其他编译器可能有自己的实现或类似的机制。
也就是说在创建线程之前要先利用_declspec(thread)关键字声明一个静态的变量,这样在创建新的线程需要申请内存的时候就可以通过这个变量去自己专属的Thread Cache中申请内存了,_declspec(thread)声明的变量只会访问自己线程中的变量,不会访问别的线程中的变量,所以通过_declspec(thread)变量的访问是线程安全的,无需加锁。
5.2 ThreadCache.h
//线程缓存
class ThreadCache
{
public:
//向Thread Cache申请size个字节的内存
void* Allocate(size_t size);
//释放size个字节大小的内存到Thread Cache中
void DeAllocate(void* obj, size_t size);
//向中心缓存中下标为index位置的哈希桶中获取size个字节大小的内存块
void* FetchFromCentralCache(size_t index, size_t size);
//size代表线程申请的一个小对象的大小,自由链表太长就要向中心缓存中还一定量的内存
void ListTooLong(FreeList& list, size_t size);
private:
FreeList _freeLists[NFREELIST];//每一个FreeList都是一个自由链表,_freeLists是一个挂满了自由链表的哈希桶数组
};
//线程本地存储变量,_declspec(thread)声明的变量在每一个线程中独享一个
static _declspec(thread) ThreadCache* pTLSThreadCache = nullptr;
5.3 ThreadCache.cpp
//向Thread Cache申请size个字节的内存
void* ThreadCache::Allocate(size_t size)
{
//计算向上对齐后的大小
int alignSize = SizeClass::RoundUp(size);
//计算所需大小的对象在哪一个哈希桶里面
int index = SizeClass::Index(alignSize);
//如果对应下标的哈希桶的自由链表中有内存对象,就直接弹出一个返回
if (!_freeLists[index].Empty())
{
return _freeLists[index].Pop();
}
else//走到这里说明对应下标的哈希桶的自由链表中没有内存对象,需要找中心缓存拿
{
return FetchFromCentralCache(index, alignSize);
}
}
void ThreadCache::ListTooLong(FreeList& list, size_t size)
{
void* start = nullptr;
void* end = nullptr;
//链表太长,就取出来一部分还给centralcache
list.PopRange(start, end, list.MaxSize());
//把start到end这一段链表还给centralcache对应位置的哈希桶中
//因为CentralCache是所有线程共享一个的,所以把它设计成单例模式
//即全局只有一个对象较好,CentralCache类提供GetInstance()函数
//获取类指针调用成员函数
CentralCache::GetInstance()->ReleaseListToSpans(start, size);
}
//释放内存到Thread Cache中
void ThreadCache::DeAllocate(void* obj, size_t size)
{
assert(size <= MAX_BYTES);
assert(obj);
//计算归还的大小为size的内存块在哪一个哈希桶中
size_t index = SizeClass::Index(size);
//把obj插入到对应的哈希桶的自由链表中
_freeLists[index].Push(obj);
//如果还回来的小对象太多,导致自由链表太长,就应该还一部分给CentralCache
if (_freeLists[index].Size() >= _freeLists[index].MaxSize())
{
ListTooLong(_freeLists[index], size);
}
}
//向中心缓存获取内存块
void* ThreadCache::FetchFromCentralCache(size_t index, size_t size)
{
//一次向CentralCache申请多个size大小的对象,那么一次要申请多少个呢?
// 这要根据size的大小计算,如果size很小,那么一次就向CentralCache申请
// 多一点,如果size很大,那么一次就申请少一点,那么如何计算一次申请多少呢?
//这里需要使用慢增长的方式控制
size_t batchNum = min(_freeLists[index].MaxSize(), SizeClass::NumMoveSize(size));
if (batchNum == _freeLists[index].MaxSize())
{
_freeLists[index].MaxSize()++;//慢增长
}
//想要向CentralCache获取batchNum个size大小的对象
void* start = nullptr;
void* end = nullptr;
//实际获取到的size大小的对象的数目
int actualNum = CentralCache::GetInstance()->FetchRangeObj(start, end, batchNum, size);
assert(actualNum > 0);
//如果只获取到了一个size大小的对象,那么就直接返回给线程了
if (actualNum == 1)
{
assert(start == end);
return start;
}
else
{
//如果获取到了多个内存块,就把start对应的内存块返回给线程
//其余的都头插到threadcache对应映射位置的哈希桶的自由链表中
/*NextObj(end) = _freeLists[index]._freeList;
_freeLists[index]._freeList = start;*/
_freeLists[index].PushRange(NextObj(start), end, actualNum - 1);
return start;
}
}