Fari

Golang

Golang GMP模型 笔记

https://www.yuque.com/aceld/golang/srxd6d

https://www.bilibili.com/video/BV19r4y1w7Nx/?p=18

本篇文章只是这个视频的一个笔记,但视频其实有非常多让人困惑的地方并没有做解释,本文也没有深究

核心在于协程调度器的优化

什么是GMP模型?

另外,还有一个全局G队列,当P队列都满了就会放到全局G队列中。

Golang调度器的设计策略

1. 线程复用

work stealing机制:当某个P队列空闲时,它会尝试从其他P队列中拉取G过来执行

hand off机制:正常情况下,一个P和一个M相绑定执行,但是当P正则执行的G发生阻塞时(例如执行read操作),它会唤醒一个新的M并将P绑定到这个新的M上执行,而原来那个M就专门负责执行阻塞的G ,如果G阻塞被唤醒了,就会重新加入到某个P队列中

2. 利用并行

可以指定多个P,充分利用多核cpu

3. 抢占

goroutine也会存在类似进程抢占cpu的机制(不是使用的队列吗?怎么也是抢占式的)

4. 全局队列

如果空闲P无法从其他P队列中stealing G,则会尝试从全局队列中stealing一个G,这一过程涉及全局队列的加锁解锁,效率不高

M0 和 G0

go程序启动后,go的进程会创建第一个线程M0,M0 也创建一个G0用于创建Golang的协程环境,例如创建P和队列等。

每个P被创建时都会绑定一个G0用于G队列的调度,G0也会负责该P队列中所有G的调度,它不会放到P队列中,当一个G执行完成后,P会先加载其对于的G0,再由G0去它的P队列中取出G来给P执行

当开始执行main函数时,就会创建一个新的G加入到某个P队列中用于执行main的内容,当碰到go语句时,也会创建G添加到P队列中,如果P队列都是满的则会加入到全局队列中

简单来说,M0就是用来做初始化环境工作的,G0主要负责P队列中G的调度

golang中可以使用 trace 或者 GODEBUG 来查看GMP的调度信息

创建G

创建时有如下几种场景或机制:

局部性:如果某正则执行的G创建了一个新的G,很多情况下,它们可能存在一些共享资源,所以新建的G应该优先放到创建它的G所在的那个P队列

队列满:一个P队列容量是有限的(默认4G内存),如果它执行的某个G创建了非常多的G,根据局部性原则,这些新建的G都应该放到这个P队列中,如果P队列已经满了的情况下又新建了一个G,则Golang会将该P队列的前面一半G连同新建的G一起打乱顺序放到全局队列中,后一半的G就会向前移动,此时P队列就空出后一半的容量,如果再有新建的G就可以向P队列后面放,再满的话重复上述过程

自旋线程:每当一个G在创建另一个G时,它都会尝试唤醒休眠线程中的一个线程,当一个线程被唤醒后,它会绑定一个P,并执行它的G0,不过由于此时它的P队列是空的,所以它没有任务执行,它就会忙循环,称为自旋线程。自旋的同时,它也会尝试从全局队列中拉取G,但它会一次拉取多个(拉取数量 = min(全局队列长度/p数量+1,全局队列长度/2)),并停止自旋。该过程称为全局队列到P本地队列的负载均衡

stealing:当一个P队列执行完了,且全局队列也是空的,它又成为了自旋线程,此时它就会尝试从其他P队列中偷后面一半的G

自旋线程限制:自旋线程+执行线程 <= $GOMAXPROCS,当已经达到最大限制后,新创建的M就会放到休眠线程队列中,因为已经没有P可以和它绑定了

如果G8发生阻塞,P2就会重新去休眠线程队列中唤醒一个队列进行绑定,而M2负责等待G8阻塞结束

当G8阻塞结束后,它并不能直接在M2上执行,因为G必须在P上才能执行

此时,M2就会尝试 1)绑定原来的P2,但此时P2已经绑定了M5,所以失败。2)从空闲P队列中绑定一个P,但此时空闲P队列是空的,所以G8就会被加到全局队列,M2会进入休眠线程队列

Golang,基于Module和基于GOPATH

本文译自:https://go.dev/ref/mod#non-module-compat

Golang的导包有两种模式,一种是基于GOPATH的(老),一种是基于Module的(新),自1.16版本后,默认开启基于Module的方式,不管 go.mod 文件存不存在

对非Module模式项目的兼容方式

对即使没有go.mod的项目(非module项目),go也能使用module模式处理package

当你需要导入的模块和其仓库根目录相同时,若在其仓库根目录没有 go.mod 文件,go就会在module cache(module下载下来后保存在本地的路径)中自动生成一个 go.mod,但其中只包含了module path(例:module golang.org/x/net),并没有包含该module依赖的其他module(即 go.mod 中的require语句),所以,当其他module需要依赖该module时就需要一些额外的require语句来获取这些依赖(即在require后面跟着 // indirect 的那些个依赖)

版本号兼容

对于版本号大于1的module,需在其module path上加上版本号后缀,例如 /golang.org/x/v2/net 表示x这个module的第2个版本(这里的v2不会被当作是目录名)

但对于一些较老的module,它已经迭代了很多版本了,但当时还没有这个规定。可以使用 +incompatible 来兼容它们。例如 require example.com/m v4.1.2+incompatible

基于module的命令

绝大多数命令都可以运行在这两个模式下,在module模式下,go命令使用 go.mod 文件去寻找依赖,一般情况下都是在 GOPATH/pkg/mod 目录下寻找,如果找不到也会下载到该目录下。在GOPATH模式下,go命令会在 vendor 目录和 GOPATH 目录下寻找依赖

在go 1.16 版本后,不管是否存在go.mod文件都会使用module模式(早期会根据是否存在该文件而做判断)

在module模式下,GOPATH不再作为编译时导包方式了,但它仍然用作存储下载下来的依赖目录(GOPATH/pkg/mod 目录)和install之后的文件保存目录(GOPATH/bin)

编译命令

一些加载包信息的命令都可以使用module模式,其中包括