目录
1、goroutine GMP模型
goroutine是用户态"线程",开销非常小,最新golang版本默认为goroutine分配的初始栈大小为2k,同时会根据运行状况动态扩展或收缩
1. G代表一个goroutine对象,每次go调用的时候,都会创建一个G对象
2. M代表一个线程,每次创建一个M的时候,都会有一个底层线程创建;所有的G任务,最终还是在M上执行
3. P代表一个处理器,每一个运行的M都必须绑定一个P,就像线程必须在么一个CPU核上执行一样
全局队列(Global Queue):存放等待运行的G
P的本地队列:同全局队列类似,存放的也是等待运行的G,存的数量有限,不超过256个。新建G时,G优先加入到P的本地队列,如果队列满了,则会把本地队列中一半的G移动到全局队列。(P列表:所有的P都在程序启动时创建,并保存在数组中,最多有
GOMAXPROCS
(可配置)个)M:内核级线程,线程想运行任务就得获取P,从P的本地队列获取G,P队列为空时,M也会尝试从全局队列拿一批G放到P的本地队列,或从其他P的本地队列偷一半放到自己P的本地队列。M运行G,G执行之后,M会从P获取下一个G,不断重复下去。
M调度策略:
1. work stealing机制(窃取式)
当本线程无G可运行时,尝试从其他线程绑定的P窃取G,而不是直接销毁线程。
2. hand off机制
当本线程M因为G进行的系统调用阻塞,线程释放绑定的P,把P转移给其他空闲的M执行。时间片:
协程的切换时间片是10ms,也就是说 goroutine 最多执行10ms就会被 M 切换到下一个 G。这个过程又被称为 中断,挂起
2、goroutine阻塞的处理
- blocking syscall (for example opening a file) // 系统调用
- network input // 网络IO
- channel operations
- primitives in the sync package // 原子、互斥量或通道操作调用
4种阻塞可以分为两类:
分类1 (对应情况2,3,4): (只G阻塞,M,P可用的,要利用起来)
1.1 用户代码层面的阻塞(channel,锁), 此时M可以换上其他G继续执行。
1.2 网络阻塞 (netpoller实现G网络阻塞不会导致M被阻塞,仅阻塞G)。
1.3 原子、互斥量或通道操作调用
1.4 由于调用time.Sleep或者ticker计时器会导致Goroutine阻塞
分类2 (对应情况1): (G,M都被阻塞,P可用,要利用起来)
2.1 系统调用(open file)
相关链接介绍:https://blog.csdn.net/jqsfjqsf/article/details/113805659
3、goroutine内存泄漏
1. goroutine内进行channel/mutex/select等读写操作被一直阻塞
2. goroutine内的业务逻辑进入死循环,资源一直无法释放
3. goroutine内的业务逻辑进入长时间等待,且有不断新增的goroutine进入等待
4. i/o的影响过高,导致耗时长,且有不断新增的goroutine进入等待
5. time.After设置Duration超时过长、time.NewTicker定时没有执行Stop方法相关链接介绍:https://blog.csdn.net/weixin_38299404/article/details/126805554
4、go抢占式调度
golang v1.14前基于协作抢占调度
golang v1.14 基于信号抢占式调度
5、map原理、扩容
5.1 map扩容
map结构体介绍
map涉及到的结构体:
hmap(哈希表,含指向桶数组的指针)
bmap(桶)
mapextra(溢出桶,根据bmap字段overflow关联)
相关介绍链接:
http://lihuaxi.xjx100.cn/news/60892.html
https://blog.csdn.net/weixin_52690231/article/details/125262099?spm=1001.2014.3001.5502
// A header for a Go map.
type hmap struct {
// 元素个数,调用 len(map) 时,直接返回此值
count int
// map是否处于写入的状态,1为在写
flags uint8
// buckets 数组的长度的对数
B uint8
// overflow 的 bucket 近似数
noverflow uint16
// 计算 key 的哈希的时候会传入哈希函数
hash0 uint32
// 指向buckets数组的指针,数组大小为2^B,如果元素个数为0,指向nil
buckets unsafe.Pointer
// 等量扩容的时候,buckets 长度和 oldbuckets 相等
// 双倍扩容的时候,buckets 长度会是 oldbuckets 的两倍
oldbuckets unsafe.Pointer
// 指示扩容进度,小于此地址的 buckets 迁移完成
nevacuate uintptr
// 存储溢出桶,为了优化GC扫描而设计
extra *mapextra // optional fields
}
buckets 是一个指针,最终它指向的是一个结构体:
type bmap struct{
tophash [bucketCnt]uint8
// len为8的数组
// 一个桶最多8个槽位
// 跟据哈希值的高 8 位,找到此key在bucket中的位置
// 最后B个bit位,计算key落在哪个桶,如果B=5,那么桶的数量就是buckets数组的长度是 2^5 = 32
// 10010111 | 000011110110110010001111001010100010010110010101010 │ 01010
}
但这只是表面(src/runtime/hashmap.go)的结构,编译期间会给它动态地创建一个新的结构:
type bmap struct{
tophash [8]uint8
//keytype由编译器编译时确定
keys [8]keytype
//elemtype由编译器编译时确定
values [8]elemtype
//overflow指向下一个bmap,overflow是uintptr而不是*bmap类型,是为了减少gc,溢出桶存储到extra字段中
overflow uintptr
}
overflow存储到extra的结构体mapextra
type mapextra struct {
// overflow[0] contains overflow buckets for hmap.buckets.
// overflow[1] contains overflow buckets for hmap.oldbuckets.
overflow [2]*[]*bmap
// nextOverflow 包含空闲的 overflow bucket,这是预分配的 bucket
nextOverflow *bmap
}
5.2 map扩容
1、双倍扩容
装载因子超过阈值,源码里定义的阈值是 6.5,触发double扩容
2、等量扩容
2.1、当 B <= 15,overflow 的 bucket 数量>= 2^B
2.2、当 B > 15,overflow 的 bucket 数量>= 2^15
备注:B 是 buckets 数组的长度的对数,也就是说 buckets 数组的长度就是 2^B
Go map 的扩容采取了一种称为“渐进式”地方式,原有的 key 并不会一次性搬迁完毕,每次最多只会搬迁 2 个 bucket。在搬迁过程中,oldbuckets 指针还会指向原来老的 []bmap,并且已经搬迁完毕的 key 的 tophash 值会是一个状态值,表示 key 的搬迁去向。hmap值nevacuate 标识的是当前的进度
6、go内存管理
Go语言的内存分配器采用了跟 tcmalloc 库相同的实现,分为四部分内容
- mspan
- mcache
- mcentral
- mheap
1、mspan
span是内存管理的基本单位,代码中为mspan
,一组连续的Page组成1个Span。mspan其实是一个双向链表的结构2、mcache
各线程需要内存时从mcentral管理的span中申请内存,为了避免多线程申请内存时不断地加锁,Golang为每个线程分配了span的缓存,这个缓存即是cache。mcache保存的是各种大小的Span,并按Span class分类,小对象(<=32KB)直接从mcache分配内存,它起到了缓存的作用,并且可以无锁访问。
mcache是每个逻辑处理器(P)的本地内存线程缓存。Go中是每个P拥有1个mcache。
mcache中每个级别的Span有2类数组链表,一个不带指针,一个带指针需要GC扫码。
3、mcentral
它按Span class对Span分类,串联成链表,当mcache的某个级别Span的内存被分配光时,它会向mcentral申请1个当前级别的Span。所有线程共享的缓存,需要加锁访问。4、mheap
它把从OS申请出的内存页组织成Span,并保存起来。当mcentral的Span不够用时会向mheap申请,mheap的Span不够用时会向OS申请,向OS的内存申请是按页来的,然后把申请来的内存页生成Span组织起来,同样也是需要加锁访问的。大对象(>32KB)直接从mheap上分配。总结
Golang内存分配是个相当复杂的过程,其中还掺杂了GC的处理,这里仅仅对其关键数据结构进行了说明,了解其原理而又不至于深陷实现细节。
- Golang程序启动时申请一大块内存,并划分成spans、bitmap、arena区域
- arena区域按页划分成一个个小块
- span管理一个或多个页
- mcentral管理多个span供线程申请使用
- mcache作为线程私有资源,资源来源于mcentral
7、go GC
golang v1.3:标记清除进行垃圾回收
golang v1.5:三色并发标记清除进行垃圾回收
golang v1.8:三色标记清除-混合写屏障
golang v1.8操作步骤:
1、GC开始将栈上的对象全部扫描并标记为黑色(之后不再进行第二次重复扫描,无需STW)
2、GC期间,任何在栈上创建的新对象,均为黑色
3、被删除的对象标记为灰色。
4、被添加的对象标记为灰色。
分析:
- 第一步:相当于栈上的删除写屏障:保证了不会原来被栈引用的栈对象被删除引用后,又被其他栈上对象引用,但是由于没有删除写屏障却被回收
- 第二步:相当于栈上的插入写屏障:保证了栈上新加对象不会由于没有写入屏障,从而被删除引用的时候被错误回收
- 第三步:满足删除写屏障
- 第四步:满足插入写屏障