golang知识点整理

目录

1、goroutine GMP模型

2、goroutine阻塞的处理

3、goroutine内存泄漏

4、go抢占式调度

5、map原理、扩容

5.1 map扩容

5.2 map扩容 

6、go内存管理

7、go GC


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阻塞的处理

  1. blocking syscall (for example opening a file) // 系统调用
  2. network input // 网络IO
  3. channel operations
  4. 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结构体介绍

hashmap bmap

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的处理,这里仅仅对其关键数据结构进行了说明,了解其原理而又不至于深陷实现细节。

  1. Golang程序启动时申请一大块内存,并划分成spans、bitmap、arena区域
  2. arena区域按页划分成一个个小块
  3. span管理一个或多个页
  4. mcentral管理多个span供线程申请使用
  5. mcache作为线程私有资源,资源来源于mcentral

7、go GC

golang v1.3:标记清除进行垃圾回收

golang v1.5:三色并发标记清除进行垃圾回收

golang v1.8:三色标记清除-混合写屏障

golang v1.8操作步骤:

1、GC开始将栈上的对象全部扫描并标记为黑色(之后不再进行第二次重复扫描,无需STW)

2、GC期间,任何在栈上创建的新对象,均为黑色

3、被删除的对象标记为灰色。

4、被添加的对象标记为灰色。

分析:

  • 第一步:相当于栈上的删除写屏障:保证了不会原来被栈引用的栈对象被删除引用后,又被其他栈上对象引用,但是由于没有删除写屏障却被回收
  • 第二步:相当于栈上的插入写屏障:保证了栈上新加对象不会由于没有写入屏障,从而被删除引用的时候被错误回收
  • 第三步:满足删除写屏障
  • 第四步:满足插入写屏障

相关链接介绍:https://zhuanlan.zhihu.com/p/377804157

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值