【C++高并发内存池】 2.ThreadCache设计

目录

前言:

1.Common.hpp

2.UnitTest.cpp

[3. ConCurrentAlloc.h](#3. ConCurrentAlloc.h)

[4. ThreadCache.h](#4. ThreadCache.h)

5.ThreadCache.cpp

设计总结:


前言:

前面一个设计了一个定长内存池,经过与传统的内存申请方式对比,初步实现了一个可承受频繁申请与释放的高性能的内存池,这是我们首先要看到的一个内存池被设计出来实现的可观效果。从这里开始,我们将一步步的对高并发内存池进行由浅入深的设计,本节我们来设计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向中心缓存申请,由此引入中心缓存的结构设计,这个由下一篇详细介绍,目前项目到这里大概就是这么个思路,后续敬请期待~~

相关推荐
Dovis(誓平步青云)2 小时前
拖动关键帧角色就抽动,动画编辑器怎样补齐中间状态
开发语言·前端·javascript·机器学习·编辑器
行走的陀螺仪2 小时前
Claude Code 从入门到实战(二):Bash工具、Plan模式与后台任务
开发语言·bash
東隅已逝,桑榆非晚2 小时前
红黑树:从概念到实现
数据结构·c++·笔记·学习
努力努力再努力wz2 小时前
【Lua 入门系列】从执行模型到事件驱动:一文理解 Lua 有栈协程、挂起恢复与协作式调度
开发语言·lua
椒颜皮皮虾྅2 小时前
一套 C# 接口,解锁四类视觉任务:DeploySharp 分类、分割、关键点与旋转框实战
开发语言·分类·c#
2603_965896622 小时前
JavaScript 原型与原型链
开发语言·前端·javascript
艾莉丝努力练剑2 小时前
【AI大模型接入SDK】ChatServer接口测试
网络·c++·人工智能·学习·ai·大模型·sdk
朋友圈自动点赞工具2 小时前
Ruby方法查找:对象不存方法
开发语言·前端·ruby·科技资讯
D e re k ZH3 小时前
C++ 模板与泛型编程进阶(No 2)
开发语言·c++