003 高并发内存池_整体框架设计
前言
在本文中,重点是向你介绍ThreadCache、CentralCache、PageCache三个结构是怎样设计的
这三个结构对于我们代码的撰写至关重要
一、ThreadCache整体框架设计
线程缓存结构主要是解决锁竞争的问题,只要当前threadcache有空间,就不会跟其它线程竞争锁,线程缓存是每一个线程独享的结构,线程申请和释放内存都在这个缓存中进行,只能用于小于256KB的内存分配.整个线程缓存结构是不用加锁的。
定长内存池只支持固定大小内存块的申请释放,因此定长内存池中只需要一个自由链表管理释放回来的内存块。现在我们要支持申请和释放不同大小的内存块,那么我们就需要多个自由链表来管理释放回来的内存块,因此threadcache实际上一个哈希桶结构,每个桶中存放的都是一个自由链表。
threadcache支持小于等于256KB内存的申请,如果我们将每种字节数的内存块都用一个自由链表进行管理的话,那么此时我们就需要20多万个自由链表,光是存储这些自由链表的头指针就需要消耗大量内存,这显然是得不偿失的。
这时我们可以选择做一些平衡的牺牲,让这些字节数按照某种规则进行对齐,例如我们让这些字节数都按照8字节进行向上对齐,那么threadcache的结构就是下面这样的,此时当线程申请1-8字节的内存时会直接给出8字节,而当线程申请9~16字节的内存时会直接给出16字节,以此类推
申请内存:
1、当内存申请size<=256KB,先获取到线程本地存储的thread cache对象,计算size映射的哈希桶自 由链表下标i。
2、如果自由链表_freeLists[i]中有对象,则直接Pop一个内存对象返回。
3、如果_freeLists[i]中没有对象时,则批量从central cache中获取一定数量的对象,插入到自由链表 并返回一个对象。
释放内存:
1、当释放内存小于256k时将内存释放回thread cache,计算size映射自由链表桶位置i,将对象Push到_freeLists[i]。
2、当链表的长度过长,则回收一部分内存对象到central cache。
二、CentralCache整体框架设计
当线程申请某一大小的内存时,如果threadcache中对应的自由链表不为空,那么直接取出一个内存块进行返回即可,但如果此时该自由链表为空,那么这时threadcache就需要向central cache申请内存了。
central cache的结构与threadcache是一样的,它们都是哈希桶的结构,并且它们遵循的对齐映射规则都是一样的,八字节为一个桶,不同的是,在CentralCache中,比如8Byte这个桶中装的是一个个Span,每个span管理的都是一个以页为单位的大块内存,每个桶里面的若干span是按照双链表的形式链接起来的,并且每个span里面还有一个自由链表,这个自由链表里面挂的就是一个个切好了的内存块,根据其所在的哈希桶这些内存块被切成了对应的大小。这样做的好处就是,当threadcache的某个桶中没有内存了,就可以直接到central cache中对应的哈希桶里去取内存就行了。
申请内存:
1、当thread cache中没有内存时,就会批量向central cache申请一些内存对象,这里的批量获取对象的数量使用了类似网络tcp协议拥塞控制的慢开始算法;central cache也有一个哈希映射的
2、spanlist,spanlist中挂着span,从span中取出对象给thread cache,这个过程是需要加锁的,不过这里使用的是一个桶锁,尽可能提高效率。 central cache映射的spanlist中所有span的都没有内存以后,则需要向pagecache申请一个新的 span对象,拿到span以后将span管理的内存按大小切好作为自由链表链接到一起。然后从span中取对象给thread cache。
3、centralcache的中挂的span中use_count记录分配了多少个对象出去,分配一个对象给thread cache,就++use_count
释放内存:
1、当thread_cache过长或者线程销毁,则会将内存释放回central cache中的,释放回来时-use_count。当use_count减到0时则表示所有对象都回到了span,则将span释放回page cache,pagecache中会对前后相邻的空闲页进行合并。
三、PageCache整体框架设计
页缓存是在central cache缓存上面的一层缓存,存储的内存是以页为单位存储及分配的,centralcache没有内存对象时,从page cache分配出一定数量的page,并切割成定长大小的小块内存,分配给centralcache。central cache的映射规则与thread cache保持一致,而page cache的映射规则与它们都不相同。pagecache的哈希桶映射规则采用的是直接定址法,比如1号桶挂的都是1页的span,2号桶挂的都是2页的span,以此类推
至于page cache当中究竟有多少个桶,这就要看你最大想挂几页的span了,这里我们就最大挂128页的span,为了让桶号与页号对应起来,我们可以将第0号桶空出来不用,因此我们需要将哈希桶的个数设置为129。
为什么这里最大挂128页的span呢?因为线程申请单个对象最大是256KB,而128页可以被切成4个256KB的对象,因此是足够的。当然,如果你想在page cache中挂更大的span也是可以的,根据具体的需求进行设置就行了
申请内存:
1、当central cache向page cache申请内存时,page cache先检查对应位置有没有span,如果没有则向更大页寻找一个span,如果找到则分裂成两个。比如:申请的是4页page,4页page后面没有挂span,则向后面寻找更大的span,假设在10页page位置找到一个span,则将10页page span分裂为一个4页pagespan和一个6页page span。
2、如果找到_spanList[1,128]都没有合适的span,则向系统使用mmap、brk或者是VirtualAlloc等方式申请128页page span挂在自由链表中,再重复1中的过程。
3、需要注意的是central cache和page cache的核心结构都是spanlist的哈希桶,但是他们是有本质 区别的,central cache中哈希桶,是按跟threadcache一样的大小对齐关系映射的,他的spanlist 中挂的span中的内存都被按映射关系切好链接成小块内存的自由链表。而pagecache中的spanlist则是按下标桶号映射的,也就是说第i号桶中挂的span都是i页内存。
释放内存:
1、 如果central cache释放回一个span,则依次寻找span的前后page id的没有在使用的空闲span,看是否可以合并,如果合并继续向前寻找。这样就可以将切小的内存合并收缩成大的span,减少内存碎片。
小结
今日的分享就到这里啦,虽然没有任何的代码,但是大家一定一定要掌握好三层内存申请的结构,这对我们后面理解以及撰写代码非常重要,后面,我将会把这三层申请内存的架构再详细介绍给你