Go语言——goroutine并发模型

Python016

Go语言——goroutine并发模型,第1张

我喜欢jetbrains系列的IDE+go插件。不过我要说的是这个问题主要看你的观点如何。

说eclipse:

构建方式是使用go install 命令,每一次编译运行都是go install。这样的好处就是如果你有很多的包,下载下来并没有编译,这样每次编译速度是很快的。而且(!)go install 符合go官方的项目结构,官方说过了,一个go的项目应该是以个gopath,包含src,pkg,bin三个主要目录。所以说go install个人认为才是主要的go编译方式。

说eclipse的缺点:

其实eclipse插件的go编译方式,还有目录结构,项目结构,都是非常完美的!!!!真的很完美!可是,他的代码提示,太差件!大括号都不能自动补全,gdb 32bit 64bit兼容问题,eclipseC++ 没有html js插件,需要手动安装,几乎不能开箱即用。不过如果你是开发算法,数据处理,还是推荐eclipse的,毕竟其他都无关紧要。

说jetbrains:

说先说clione肯定不适合,新建项目没有向导,导致改成go项目各种不开心,比如图标对于我来说就无法接受go lib 不是小耗子~这是次要的,重要的是各个文件都是灰色的(没有在cmake中包含的结果),然后说剩下的,phpstorm这个不说了,估计很少有人插件按在这里,webstorm,体验也不是很好,idea?体验很好,可是毕竟比较重,尤其是现在加入了自家的K啥玩意(无意冒犯,没记住单词)~可是话说回来,go跟C系列IDE配合才是最佳,跟java系列一点不搭关系,用idea似乎有点格格不入,但是!idea支持新建项目向导,lib的图标也很清晰,最后还是选择idea吧,期待clion的强大起来!

再说jetbrains系列缺点:

插件的构建方式是go buiild 这个让人很不爽,我们几乎不确定会构建到什么地方去,还要每次设置一下run配置。这个可能无关紧要,毕竟不是什么大的毛病,可是go build不能缓存.a文件,直接构建的结果就是很多第三方包的情况下很慢!所以建议安装包的时候手动install 一下解决这个问题。自带代码格式化,这个格式化跟go 格格不入,总的来说就是蛋疼,心碎,菊花痒。

最后说liteIDE:

轻量级IDE,我可以说是国人GO伟大作品典范,然而默认构建也是go build,项目管理方式不符合go官方标准。代码提示不能自动导入(eclipse也不能),不过如果你的项目是以包为单位的,那么另当别论。一定很不错,毕竟是轻量级专门针对GO的IDE!

说这些,其实还有很大一部分取决于你的项目是用vendor机制管理,还是godeps机制管理依赖关系。go不像java拥有强大的几乎天下一统的maven(无意冒犯,暂不评价其他构建套件)。

go没有官方包仓库。

go没有官方包管理工具。

go没有官方自动化构建套件。

上面三个没有是致命要害。导致民间各种百花齐放。

说说我的项目怎么管理

gpm 一个shell工具(windows下你可以用git的bash,或者cygwin~)

我是严格艳照官方推荐方式管理go项目,一个go项目一个gopath。系统的gopath只是为了安装go命令,我没有配置gobin,意义不大。

项目的依赖跟我的代码包都在src下(非vendor)

vendor用来存放包的特殊依赖,发布项目直接把依赖包发布上去(公网管理则只上传依赖关系文件 godeps文件)

资源文件等都放在src目录同级,编译文件放在bin,引用直接../引用。

golang是一门自带垃圾回收的语言,它的内存分配器和tmalloc(thread-caching malloc)很像,大多数情况下是不需要用户自己管理内存的。最近了解了一下golang内存管理,写出来分享一下,不正确的地方请大佬们指出。

1.内存池:

应该有一个主要管理内存分配的部分,向系统申请大块内存,然后进行管理和分配。

2.垃圾回收:

当分配的内存使用完之后,不直接归还给系统,而是归还给内存池,方便进行下一次复用。至于垃圾回收选择标记回收,还是分代回收算法应该符合语言设计初衷吧。

3.大小切分:

使用单独的数组或者链表,把需要申请的内存大小向上取整,直接从这个数组或链表拿出对应的大小内存块,方便分配内存。大的对象以页申请内存,小的对象以块来申请,避免内存碎片,提高内存使用率。

4.多线程管理:

每个线程应该有自己的内存块,这样避免同时访问共享区的时候加锁,提升语言的并发性,线程之间通信使用消息队列的形式,一定不要使用共享内存的方式。提供全局性的分配链,如果线程内存不够用了,可向分配链申请内存。

这样的内存分配设计涵盖了大部分语言的,上面的想法其实是把golang语言内存分配抽象出来。其实Java语言也可以以同样的方式理解。内存池就是JVM堆,主要负责申请大块内存;多线程管理方面是使用栈内存,每个线程有自己独立的栈内存进行管理。

golang内存分配器

golang内存分配器主要包含三个数据结构:MHeap,MCentral以及MCache

1.MHeap:分配堆,主要是负责向系统申请大块的内存,为下层MCentral和MCache提供内存服务。他管理的基本单位是MSpan(若干连续内存页的数据结构)

type MSpan struct

{

MSpan   *next

MSpan   *prev

PageId  start // 开始的页号

uintptr   npages// 页数

…..

}

可以看出MSpan是一个双端链表的形式,里面存储了它的一些位置信息。

通过一个基地址+(页号*页大小),就可以定位到这个MSpan的实际内存空间。

type MHeap struct

{

lock  mutex

free  [_MaxMHeapList] mSpanList  // free lists of given length

freelarge mSpanList   // free lists length >= _MaxMHeapList

busy  [_MaxMHeapList] mSpanList   // busy lists of large objects of given length

busylarge mSpanList

}

free数组以span为序号管理多个链表。当central需要时,只需从free找到页数合适的链表。large链表用于保存所有超出free和busy页数限制的MSpan。

MHeap示意图:

2.MCache:运行时分配池,不针对全局,而是每个线程都有自己的局部内存缓存MCache,他是实现goroutine高并发的重要因素,因为分配小对象可直接从MCache中分配,不用加锁,提升了并发效率。

type MCache struct

{

tiny   byte* // Allocator cache for tiny objects w/o pointers.

tinysize   uintptr

alloc[NumSizeClasses] MSpan* // spans to allocate from

}

尽可能将微小对象组合到一个tiny块中,提高性能。

alloc[]用于分配对象,如果没有了,则可以向对应的MCentral获取新的Span进行操作。

线程中分配小对象(16~32K)的过程:

对于

size 介于 16 ~ 32K byte 的内存分配先计算应该分配的 sizeclass,然后去 mcache 里面

alloc[sizeclass] 申请,如果 mcache.alloc[sizeclass] 不足以申请,则 mcache 向 mcentral

申请mcentral 给 mcache 分配完之后会判断自己需不需要扩充,如果需要则想 mheap 申请。

每个线程内申请内存是逐级向上的,首先看MCache是否有足够空间,没有就像MCentral申请,再没有就像MHeap,MHeap向系统申请内存空间。

3.MCentral:作为MHeap和MCache的承上启下的连接。承上,从MHeap申请MSpan;启下,将MSpan划分为各种尺寸的对象提供给MCache使用。

type MCentral struct

{

lock mutex

sizeClass int32

noempty mSpanList

empty mSpanList

int32 nfree

……

}

type mSpanList struct {

first *mSpan

last  *mSpan

}

sizeclass: 也有成员 sizeclass,用于将MSpan进行切分。

lock: 因为会有多个 P 过来竞争。

nonempty: mspan 的双向链表,当前 mcentral 中可用的 mSpan list。

empty: 已经被使用的,可以认为是一种对所有 mSpan 的 track。MCentral存在于MHeap内。

给对象 object 分配内存的主要流程:

1.object size >32K,则使用 mheap 直接分配。

2.object size <16 byte,使用 mcache 的小对象分配器 tiny 直接分配。 (其实 tiny 就是一个指针,暂且这么说吧。)

3.object size >16 byte &&size <=32K byte 时,先使用 mcache 中对应的 size class 分配。

4.如果 mcache 对应的 size class 的 span 已经没有可用的块,则向 mcentral 请求。

5.如果 mcentral 也没有可用的块,则向 mheap 申请,并切分。

6.如果 mheap 也没有合适的 span,则想操作系统申请。

tcmalloc内存分配器介绍

tcmalloc(thread-caching mallo)是google推出的一种内存分配器。

具体策略:全局缓存堆和进程的私有缓存。

1.对于一些小容量的内存申请试用进程的私有缓存,私有缓存不足的时候可以再从全局缓存申请一部分作为私有缓存。

2.对于大容量的内存申请则需要从全局缓存中进行申请。而大小容量的边界就是32k。缓存的组织方式是一个单链表数组,数组的每个元素是一个单链表,链表中的每个元素具有相同的大小。

golang语言中MHeap就是全局缓存堆,MCache作为线程私有缓存。

在文章开头说过,内存池就是利用MHeap实现,大小切分则是在申请内存的时候就做了,同时MCache分配内存时,可以用MCentral去取对应的sizeClass,多线程管理方面则是通过MCache去实现。

总结:

1.MHeap是一个全局变量,负责向系统申请内存,mallocinit()函数进行初始化。如果分配内存对象大于32K直接向MHeap申请。

2.MCache线程级别管理内存池,关联结构体P,主要是负责线程内部内存申请。

3.MCentral连接MHeap与MCache的,MCache内存不够则向MCentral申请,MCentral不够时向MHeap申请内存。