Golang
Golang GMP模型 笔记
https://www.yuque.com/aceld/golang/srxd6d
https://www.bilibili.com/video/BV19r4y1w7Nx/?p=18
本篇文章只是这个视频的一个笔记,但视频其实有非常多让人困惑的地方并没有做解释,本文也没有深究
核心在于协程调度器的优化
什么是GMP模型?
-
G(goroutine):协程
-
P(Processor):协程处理器,一次只能处理一个协程,每个P都有一个协程队列,用于存放待处理的协程。P的数量可以通过环境变量或代码进行设置,它的数量表示Golang在某时刻支持的最大并行量
-
M(Thread):内核态线程
另外,还有一个全局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模式(早期会根据是否存在该文件而做判断)
-
当
GO111MODULE=off
,go命令会忽视 go.mod 文件直接使用GOPATH模式 -
当
GO111MODULE=on
或者为空时,则会使用module模式,即使 go.mod 文件不存在 -
当
GO111MODULE=auto
,若当前文件夹或任何父文件夹存在 go.mod 文件,则使用module模式
在module模式下,GOPATH不再作为编译时导包方式了,但它仍然用作存储下载下来的依赖目录(GOPATH/pkg/mod 目录)和install之后的文件保存目录(GOPATH/bin)
编译命令
一些加载包信息的命令都可以使用module模式,其中包括
-
go build
-
go fix
-
go generate
-
go get
-
go install
-
go list
How go module "import" works?
此文翻译自官方文档:https://go.dev/ref/mod#resolve-pkg-mod
当你使用 import xxx 方式导包时,它首先需要判断这个包来自哪个module
go首先会在 build list寻找是否有哪个module的前缀与导入的包前缀的相同,例如,如果你想导入
example.com/a/b
,并且example.com/a
存在与 build list(go.mod中列出的依赖,类似 package.json 或者 build.gradle 这种),然后go将会检查example.com/a
是否存在b
这个目录,且其下面至少包含一个go文件。如果在build list中找到匹配的module的话就使用它,如果没找到或者找到了多个module都提供了这个package(为啥会这样?),go就会报错。但如果设定了-mod=mod
参数的话,go就会尝试去寻找这个包,go get
和go mod tidy
命令会自动做这件事情怎么找呢?它会首先检查
GOPROXY
这个环境变量,该命令可以设置多个代理地址,使用逗号分开。或是将其设置为off
,表示不使用代理。他会遍历所有的代理地址,并寻找所有可能提供该pkg的module(就是所有的pkg前缀都当作module名尝试寻找),对于每个可能的module,go都会将其最新版本下载下来,并且去看它是否真的提供所需的pkg。如果有多个module都提供了这个包,则以名称长的那个module为准。如果都不提供或者找不到可能的module,则报错。
例如:你的代码中
import golang.org/x/net/html
并且
GOPROXY=https://corp.example.com,https://proxy.golang.org
go就会同时在两个代理服务器中找:
- 在 https://corp.example.com/ 中(并发寻找多个可能的module):
-
golang.org/x/net/html
-
golang.org/x/net
-
golang.org/x
-
golang.org
-
如果在第一个代理服务器中没找到,则继续按上述寻找方式在第二个代理服务器中找,以此类推
找到后则会下载并更新
go.mod
和go.sum
文件,go会在go.mod
中新增一个关于该module路径和版本的requirement
语句,如果寻找的这个package不是直接被 main module 使用,这条新加的 requirement 语句后面就会跟上一个// indirect
的注释Golang基础知识
优势
-
简单的部署方式
-
可直接编译成机器码
-
不依赖其他库
-
直接运行即可部署
-
-
静态类型语言
- 编译的时候检查出大多数的问题
-
语言层面的并发
-
强大的标准库
-
runtime系统调度机制
-
高效的GC垃圾回收
-
丰富的标准库
-
-
简单易学
-
25个关键字
-
c语言支持
-
面向对象
-
跨平台
-
成就
-
Docker
-
Kubernetes
缺点
-
包管理,大部分都托管在github上
-
无泛化类型
-
所有的Exception都用Error来处理
-
对C的降级处理并非无缝,没有C降级到asm(汇编)那么完美
杂记
-
分号可加可不加
-
左花括号一定和方法名在同一行(同java,但强制)
变量
声明变量的四种方式
-
var a int // 默认值为0
-
var a int = 100 // 初始化值
-
var c = 100 //不显示声明类型
-
c := 100 // 最常用,省去var
注:声明全局变量,以上方法1、2、3都可以,
:=
只能使用在函数体中- 多变量声明
- 方式一
var v1, v2 = 100, "name"
- 方式二
var ( v1 int = 100 v2 bool = true )
常量
将变量中的var修改为const即可
使用Go module创建项目
项目实例
最终目录结构
创建项目目录
创建一个目录作为项目根目录(目录名随意,我这里使用 go_practice ),在在其下面创建一个子目录(sub_pkg)用于演示package的引用方式
初始化GOMODULE
在 go_practice 目录下执行命令
go mod init hunt/practice
其中
hunt/practice
是该 module 名,其他 module 需要引入该 module 时就使用该名称引入,该名称可设置为任何合法字符串 “mypractice”、“dir1/dir2/dir3”、“github.com/some/dependency” 等。命令执行完成之后会在该目录下生成一个
go.mod
文件,该文件维护该module的依赖(初始化项目没有依赖,所有是空的)创建go文件
首先在
sub_pkg
下创建一个say_hi.go
,用于演示调用本地package的情况// 包名建议和目录名相同,此处为了演示效果设置一个不同的名称 package hi import ( "fmt" ) // 方法名开头大写可被其他package调用,否则视为private只能同包使用 func Say() { fmt.Println("hi") }
再在
sub_pkg
下创建一个thirdpart.go
,用于演示调用第三方库的情况// 注意,同一个目录下的go文件同属于同一个package,故设置package名相同(若设置不同名称会编译报错) package hi import ( "fmt" "rsc.io/quote" ) func Third_say() { fmt.Println("third part pkg say: " + quote.Go()) }
然后在根目录 go_practice 下创建一个 my_main.go
- 在 https://corp.example.com/ 中(并发寻找多个可能的module):