02 线程资源层的初步设计

        粗略来讲,线程资源层负责一次性申请资源在256KB的请求,当内存不足时向中心资源层申请资源,并在合适的时机归还资源。本节主要完成线程资源的初步框架,它的后续部分要在其它层完成才能跟进。

 线程资源层总体结构图:

一、线程资源池——ThreadCache

        每个线程都应该拥有一个独有的资源池,它就是ThreadCache。ThreadCache有208个桶组成,它们组成一个数组,每个桶内装有一定数量的内存块,不同桶的内存块大小不同;当应用像线程资源层申请内存空间时,就挑选一个合适大小的桶,从桶内取出一个内存块给它;比如申请10字节空间,就从16字节的桶内取出一个16字节大小的内存块,取出的内存块必须满足要求,同时应当保证不至于溢出要求太多,ThreadCache结构如图所示:

         为什么一会按8字节递增,一会按16字节,一会又按...递增呢?原因是这样做可以诗桶的总数不至于太多,16+56+56+56+24=208桶不多不少的同时,又保证内碎片最多在12%左右,例:申请1025个字节,则按照规则,分配给1152字节大的内存块,多分配了127个字节,浪费率为127 / 1152≈11%

        至于怎么给每个桶装满内存块,关系到中心资源层,将在后续章节中完成,目前就当每个桶都是空桶就好了。

二、桶的实现——FreeList

         那么该如何实现桶呢?

        借助00.基础——简单定长内存池的方式即可,创造一个桶类,其内部存放着一个void*指针,指针指向桶中第一个内存块,内存块的前4或前8个字节指向桶中的下一个内存块。。。该类还要实现内存块的分配和回收功能。本节中只实现桶类的这些基本功能即可:

static void*& NextNode(void* ptr)//参数为一个指向内存块的指针,这个内存块的前4或8个字节(指针大小)可能记录了下一个内存块的地址
{
	return *(void**)ptr;//将该指针强制视为一个指向void*指针的指针,再解引用,值即为ptr参数所指向的内存块
}						//返回值类型void*&的功能是将该内存块视为一个void*指针(4或8字节大小),这就意味着这个“指针变量”里的值是该内存块所记录的下一个内存块的地址
						//因此实现了NextNode的功能,函数返回ptr所指向内存块的下一个内存块的地址,同时以&返回意味着可以直接修改初始内存块的前4或前8个字节的值,以使它指向其他内存块

class FreeList
{
public:
	功能测试所用,实际构造函数自然不是这个
	//FreeList(size_t n,size_t number)
	//{	
	//	void* prev = malloc(n);
	//	NextNode(prev) = nullptr;
	//	for (int i = 1; i < number; i++)
	//	{
	//		void* ptr = malloc(n);
	//		NextNode(ptr) = prev;
	//		prev = ptr;
	//	}
	//	freelist=prev;
	//}
	void push(void* ptr)//回收内存块,头插进freelisy链表中
	{
		assert(ptr);
		NextNode(ptr) = freelist;
		freelist = ptr;
	}
	void* pop()//分配内存块,从freelist链表头删
	{
		assert(freelist);
		void* obj = freelist;
		freelist = NextNode(freelist);
		return obj;
	}
	bool empty()
	{
		return freelist == nullptr;
	}
private:
	void* freelist = nullptr;
};

 三、桶的恰当分配

        前文说过,“申请1025个字节,则按照规则,分配给1152字节大的内存块”,那么程序中当然要有选择合适的桶的功能,不然申请10个字节,分了个128字节的桶就太浪费了。每次选择最小能满足要求的桶,另外还要根据选择的桶的大小,来确定桶的编号。每个桶都是一个FreeList对象,所以把这总计208个桶由小到大组成一个数组,下标为0的是最小的8字节桶,下表为208-1的是最大的256*1024字节的桶。这两个要求,都不难实现:

class SizeClass
{
public:
	//桶的大小从8字节开始计算
	// 当桶的大小处于[1,128]	每层桶按8字节递增       桶的下标范围为freelist[0,16)
	// 当桶的大小处于[128+1,1024]	每层桶按16字节递增  	桶的下标范围为freelist[16,72)
	// 当桶的大小处于[1024+1,8*1024]	每层桶按128字节递增		桶的下标范围为freelist[72,128)
	// 当桶的大小处于[8*1024+1,64*1024]		每层桶按1024字节递增	桶的下标范围为freelist[128,184)
	// 当桶的大小处于[64*1024+1,256*1024]		每层桶按8*1024字节递增	  桶的下标范围为freelist[184,208)
	// 由此可以确保每个桶内最多只有10.5%左右的内碎片浪费
	static size_t _AlignSize(size_t n, size_t alignNum)
	{
		return (n + alignNum - 1) & ~(alignNum - 1);//巧妙算法,将需要对齐的数加上对齐数-1(这样就必定不会因为加的数导致对齐结果变大,且实现向上取齐),再与上对齐数减1取反(必定能将加法导致的多余字节数去除)
	}
	static size_t AlignSize(size_t n)//计算最小能满足要求的桶的大小
	{
		if (n <= 128)
			return _AlignSize(n, 8);
		else if (n <= 1024)
			return _AlignSize(n, 16);
		else if (n <= 8 * 1024)
			return _AlignSize(n, 128);
		else if (n <= 64 * 1024)
			return _AlignSize(n, 1024);
		else if (n <= 256 * 1024)
			return _AlignSize(n, 8 * 1024);
		else
			assert(false);
	}
	static size_t BucketIndex(size_t alignedSize)//确定最小能满足要求的桶的下标
	{
		static short int BucketsNumber[] = { 16,72,128,184 };//各对齐数之前含有的桶链数量,0-128有16个桶阶,129-1024有56个桶阶......,所以16+56=72,16+56+56=128,16+56*3=184
		if (alignedSize <= 128)
			return (alignedSize>>3) - 1;
		else if (alignedSize <= 1024)
			return (alignedSize - 128>>4) + BucketsNumber[0] - 1;//右移4位相当于除以16,位运算的速度比除法要快
		else if (alignedSize <= 8 * 1024)
			return (alignedSize - 1024>>7) + BucketsNumber[1] - 1;
		else if (alignedSize <= 64 * 1024)
			return (alignedSize - 8 * 1024>>10) + BucketsNumber[2] - 1;
		else if (alignedSize <= 256 * 1024)
			return (alignedSize - 64 * 1024>>13) + BucketsNumber[3] - 1;
		else
			assert(false);
	}

};

四、ThreadCache的实现

        在掌握了桶的实现后,ThreadCache也就水到渠成了。ThreadCache类中应该包含一个桶数组(FreeLists),同时提供两个接口,一个负责根据应用进程提出的空间分配要求,从合适的桶中取出内存块给予应用进程;一个负责回收内存块,当然它们底层依靠的是调用二、三节已经实现了的FreeList和SizeClass类中的接口。

const size_t ThreadCache_MaxSize = 256 * 1024;//允许向线程资源层一次性申请的最大字节数
const size_t BUCKETSIZE = 208;//桶的数量
class ThreadCache
{
public:
	void* Allocate(size_t n);
	void DeAllocate(void* ptr,size_t n);
	void* RequestFromCentralCache(size_t index, size_t alignedSize);//申请空间大于256KB时向中心资源曾申请空间,后续实现
private:
	FreeList freelists[BUCKETSIZE];
};



void* ThreadCache::Allocate(size_t n)//根据要求,确定要分配的桶的大小,并从桶中取出内存块
{
	assert(n <=ThreadCache_MaxSize);
	size_t alignedSize = SizeClass::AlignSize(n);
	size_t index = SizeClass::BucketIndex(alignedSize);
	if (freelists[index].empty())
		return RequestFromCentralCache(index, alignedSize);//如果桶空了,向中心资源层申请空间
	else
		return freelists[index].pop();
}
void ThreadCache::DeAllocate(void* ptr, size_t n)//通过调用桶类的接口,为对应的桶回收内存块
{
	assert(ptr&&n<ThreadCache_MaxSize);
	size_t alignedSize = SizeClass::AlignSize(n);
	size_t index = SizeClass::BucketIndex(alignedSize);
	freelists[index].push(ptr);
}
void* ThreadCache::RequestFromCentralCache(size_t index, size_t alignedSize)
{	
	//......后续实现
	return nullptr;
}

五、使每个线程都拥有一个ThreadCache

       当一个应用进程使用了本并发内存池时,它所拥有的线程都将独占一个线程资源池。我们知道,同一个进程的不同线程是共享资源的,因此我们不宜在全局作用域内为每个线程分配资源,可以用编译器自带的TLS--thread local storage工具来完成这个要求。

        首先,我们需要定义一个静态TLS指针变量,它指向ThreadCache类型的对象,这样的变量每个线程都是独有的:

 static _declspec(thread) ThreadCache* pTLSThreadCache = nullptr;//每个线程独有的变量

        值得一提的是,它应当是static类型的,将作用域局限于本文件内,以免链接时和其他包含了pTLSThreadCache的文件产生冲突。

        第二步,就该对ThreadCache类中分配(Allocate)和回收(DeAllocate)内存块的接口进行封装了。封装的目的是,保证后续我们调用接口时,是从每个线程自己的ThreadCache对象中调用的,以后统一调用封装后的ThreadCache接口能省下不少麻烦。

        前文,我们已经定义了一个ThreadCache*型的TLS变量pTLSThreadCache,它保证了进程不论开出了多少个线程,每个线程都能拥有一个独享的ThreadCache*变量。接下来我们实现一个接口,使得每个线程都能new出一个ThreadCache对象给pTLSThreadCache指针管理。方法很简单,封装后分配内存的接口为void* ThisThreadAllocate(size_t n),它通过调用pTLSThreadCache指针指向的ThreadCache对象的接口来完成内存分配,那么只需在函数中对pTLSThreadCache作检查,当其为空指针时便new一个ThreadCache对象即可。

        回收内存的接口同理,在内部调用pTLSThreadCache指针指向的ThreadCache对象的回收内存的接口:

//这里函数的作用是对ThreadCache里的接口做封装,使得每个线程都拥有独有的ThreadCache,并使用本文件的函数从自己的ThreadCache里申请空间
void* ThisThreadAllocate(size_t n)
{
	if (pTLSThreadCache == nullptr)
	{
		pTLSThreadCache = new ThreadCache;
	}
	return pTLSThreadCache->Allocate(n);
}
void ThisThreadDeAllocate(void* ptr, size_t n)
{	
	assert(pTLSThreadCache);
	pTLSThreadCache->DeAllocate(ptr, n);
}

        注意:回收内存的接口中一直都有两个参数,一个是指向要回收的内存块的指针prt,一个是该内存块的大小n,实际上,后者是不需要的,但是要在后续章节的内容中才能实现,所以后续会重新改写回收函数。

六、总结

        经历以上种种步骤,线程资源层的大体框架就终于完成了,最后再作一次总结梳理:

  1. 线程资源池的具象化就是ThreadCache这个类,它内部包含了208个装着内存块的大小不等的桶。
  2. 桶的具象化就是FreeList类,其内包含了一个void* freelist指针,它就是指向一个个内存块的钥匙,另外FreeList类还实现了最底层的分配与回收内存块的功能。
  3. ThreadCache类中包含了一个FreeList freelists[208]的数组,208个桶按照从小到大排列。
  4. SizeClass类是一个工具类,里面包含了几个static接口,负责完成根据需求选择大小合适的桶,并返回桶在freelists数组中的下标。
  5. ThreadCache还有两个接口,负责调用SizeClass类中和FreeList类中的接口,来完成根据需求,分配大小合适的桶中的内存块并回收内存。
  6. 最终,是由void* ThisThreadAllocate(size_t n)和void ThisThreadDeAllocate(void* ptr, size_t n)两个封装了ThreadCache类功能的接口来真正启动分配与回收内存块的功能的。这样做是为了让每个线程的ThreadCache对象互不干扰。

 线程资源层总体结构图:

       本专栏文章不提供完整的项目代码,只讲解项目中的关键之处的代码,作梳理之用,至于整个项目的完整框架及代码,后续会陆续上传到本人的Gitee仓库中。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值