项目 高并发内存池_高并发内存池实现,Golang程序员怎么优雅迈过30K+这道坎

57 阅读2分钟

当thread cache中的内存归还给central cache的时候,如果central cache中的span满足归还条件的话,就将这个span归还给page cache,然后page cache在将归还回来的内存合并成更大的页。

thread cache:

原理图:

申请内存:

  1. 当申请的内存大于64KB的话,直接使用malloc申请内存。此外当申请内存byte <= 64KB时在thread cache中申请。根据byte计算出需要在数组中的那个自由链表中申请内存,如果自由链表中有内存块的话,从_freelist[i]直接Pop拿出内存块使用,时间复杂度是O(1),没有锁竞争
  2. 当自由链表_freelist[i]中没有内存块的时候,则批量从central cache中申请一定数量的内存块,插入到自由链表,并且返回一个内存块

释放内存:

  1. 当释放内存小于64KB时,将内存释放会thread cache中,计算byte在数组中的位置,将内存块push到_freelist[i]上
  2. 当链表长度过长,则回收一部分内存块到central cache

 

控制在12%左右的内碎片浪费
[1,128]             8byte对齐     _freelist[0,16)
[129,1024]          16byte对齐    _freelist[16,72)
[1025,8*1024]       128byte对齐   _freelist[72,128)
[8*1024+1,64*1024]  512byte对齐   _freelist[128,240)
                                
内碎片的计算方法:
对于7/8;15/(128+16);127/(1024+128);511/(8*1024+512);
对于进行哈希映射的数组的大小是240
240 = (128/8) + (1024-128)/16 + (8*1024-1024)/128 + (64*1024-8*1024)/512

 

central cache:

原理图:

申请内存:

  1. 当申请的内存小于64Kb的时候,当thread cache中没有内存时,就会批量向central cache申请一些内存对象,central cache数组中每个元素是 _spanlist,每个 _spanlist下面都挂着一个个的span,从span中取出内存块来给thread cache。这个过程是要加锁的

    1. 当_spanlist[i]中有span时,并且span中是不为空的话,直接从这个span中获取内存块,否则向page cache申请一个span对象,span对象中是一些以页为单位的内存,切成需要的内存大小,并链接起来,挂到span中
  2. 当申请的内存大于64KB小于128页的时候,直接去PageCache中申请内存。这个过程需要加锁

  3. 当申请的内存大于128页的时候,就直接去系统申请内存,申请内存的时候是以页为单位的。这个过程需要加锁

释放内存:

  1. 当thread cache过长或者线程销毁的,则会将内存释放会central cache,释放回来时--usecount。当usecount减到0时,则表示所有对象都回到了span,则将span释放回page cache,page cache中会对前后相邻的空闲页进行合并,这个过程需要加锁

 

page cache:

原理图:

申请内存:

  1. 当central cache向page cache申请内存的时候,先检测对应位置有没有span,如果有直接返回这个span,如果没有的话则向更大页寻找一个更大的span,如果找到分裂成两个span
  2. 如果找到128page都没有合适的span,则向系统使用mmap、brk或者是VirtualAlloc等方式申请128page span挂在_pagelist数组里面,然后再次重复步骤一

释放内存:

  1. 如果central cache释放一个span,则依次寻找span前后的pageid的span,看是否可以进行合并,如果合并继续向前找,直到找不到可以合并的了,或者如果此次合并大于128page。这样就可以将切小的内存合并收缩成大的span,减少内存碎片。这个过程需要加锁,因为有可能是某一个线程申请的内存是大于64K但是小于128页的,这个归还的时候,也是直接归还到页上,才是就有可能造成多个线程进行访问。

小结:总共有四次加锁的过程

  1. 线程缓存到中心缓存申请内存。这是加的是桶锁,因为有可能多个线程需要的内存空间是不一样大的,所以需要访问的span链表也是不一样的,只需要当访问同一个span链表的时候在进行加锁就好了
  2. 线程缓存将内存块释放到中心缓存。这个也是增加的桶锁,只有是访问的是同一个span链表的时候才会加锁
  3. 线程直接到页缓存申请内存。对于到缓存申请内存页的时候,有可能会造成对多个页进行操作改变,所以此时要在最外面增加一个锁
  4. 从中心缓存释放span到页缓存。将span释放到页缓存的时候,会对多个span进行合并,从而也就会改变多个span所以要在最外面增加一个锁
  • 对于项目的申请内存的流程图

  • 对于项目的释放内存的流程图

优点:

  • 高并发:

    • 高并发是因为对于每一个线程都有着自己的一个线程缓存,当每一个线程申请内存的时候就不需要每次要到系统申请内存直接到自己的线程缓存上申请内存就好了。就不会牵扯到多个线程访问同一份资源,就达到了高并发的目的,使用到了静态的TLS
  • 提高效率:

    • 每一次使用内存的时候,提前将内存都已经分配好了,直接用内存就不需要再次从系统申请内存了。也就是减少了调用系统调用函数的次数,从而提高了效率。并且有着中心缓存还进行多个线程之前的均衡,不会让一个线程占用着许多个内存不使用,导致其他的线程想要申请内存的时候申请不到内存的情况。当一个线程内部的内存块大于一个水位线的时候,就将内存全都释放到中心缓存中
  • 解决了内存碎片:

    • 对于该项目将内存碎片控制在大约12%左右,关键就是对于外碎片进行了减少,因为对于线程缓存不使用的内存就会归还到中心缓存的一个span上,而中心缓存的span上的内存只要没有线程使用的话,就将这个span再次归还到页缓存上,归还到页缓存的时候就会对多个span进行合并,从而将小的内存合并成大的内存

 

项目测试:

**【注意】:**在测试的时候需要使用relase模式(发行版本)来进行测试,采用这个模式对于自己书写的程序,编译器可以对其尽可能的优化,从而达到的性能也就是最好的。

以下测试均是在VS2017下进行测试得出。

插图:Concurrentmempool测试

测试1:50个线程,100轮,每轮1000次,每次申请1024byte

测试2:10个线程,100轮,每轮1000次,每次申请1024byte

测试3:10个线程,100轮,每轮100次,每次申请528384byte

测试4:10个线程,100轮,每轮1000次,每次申请10byte

测试5:50个线程,100轮,每轮100次,每次申请10byte

测试6:1个线程,100轮,每轮100次,每次申请524288byte

 

项目不足:

  • 当前项目还是没有完全脱离使用malloc。

比如:在内存池自身的数据结构的管理当中,比如spanlist中的Span结构还是使用new Span的操作完成,而new的底层就是malloc。还有就是对于使用STL库中的unordered_map的时候,就间接使用了空间配置器。对于空间配置器底部申请内存的时候,是使用了一级空间配置器和二级空间配置器,对于一级空间配置器是使用malloc实现的。

二级空间配置器:当申请内存的时候是先到自由链表中查看是否有内存,如果有内存的话就直接使用内存,没有内存的时候就要到内存池申请内存。

  1. 一般是申请20个块内存,

  2. 如果不够20块的话有多少块申请多少块。

  3. 如果连一块都没有的话就将所有的内存都挂到自由链表上去。然后内存池再去堆申请内存

    1. 申请到内存之后再次分配给程序
    2. 没有申请到内存的话,就到自由链表上看有没有比要申请的这一块内存池大的内存,如果有的话就直接将这一块的内存释放到内存池,然后再申请内存
    3. 最后就是调用一级空间配置器

 

解决方案:

  • 项目中增加一个定长的ObjectPool的对象池,对象池的内存直接使用brk,virtuallAlloc向系统内存申请,new Span替换成对象池申请内存。对于STL中的unordered_map也是使用一个对象池来分配内存就好了。这样就完全脱离了malloc,就可以替换到malloc

 

平台及兼容性:

  • Linxu等系统下,需要将VIrtuallAlloc替换为brk
  • X64系统下,实现支持不足。不如:id查找Span的映射,我们使用的是map<id, Span*>。在64位系统下,这个数据结构在性能和内存等方面都是撑不住的。使用基数树

 

替换系统的malloc和free:

  • 在以上测试中,当前实现的并发的内存池比malloc/free是更加高效的。但是我们如何将我们写的代码替换到系统调用呢?
  • 对于不同的系统平台替换的方式是不同的。对于Linux下使用weak alias的方式进行实现的
  • 对于其他的平台可以使用hook技术来进行实现

img img img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取