1. goroutine 的使用
在 Go 语言中, 表达式 go f(x, y, z)会启动一个新的 goroutine 运行函数 f(x, y, z), 创建一个并发任务单元. 即 go 关键字可以用来开启一个 goroutine(协程))进行任务处理.
创建单个 goroutine
- 1 package main
- 2
- 3 import (
- 4 "fmt"
- 5 )
- 6
- 7 func HelloWorld() {
- 8 fmt.Println("Hello goroutine")
- 9 }
- 10
- 11 func main() {
- 12 go HelloWorld() // 开启一个新的并发运行
- time.Sleep(1*time.Second)
- 13 fmt.Println("后输出消息!")
- 14 }
输出
Hello goroutine
后输出消息!
这里的 sleep 是必须的, 否则你可能看不到 goroutine 里头的输出, 或者里面的消息后输出. 因为当 main 函数返回时, 所有的 gourutine 都是暴力终结的, 然后程序退出.
创建多个 goroutine 时
- package main
- import (
- "fmt"
- "time"
- )
- func DelayPrint() {
- for i := 1; i <= 3; i++ {
- time.Sleep(500 * time.Millisecond)
- fmt.Println(i)
- }
- }
- func HelloWorld() {
- fmt.Println("Hello goroutine")
- }
- func main() {
- go DelayPrint() // 第一个 goroutine
- go HelloWorld() // 第二个 goroutine
- time.Sleep(10*time.Second)
- fmt.Println("main func")
- }
输出
- Hello goroutine
- 1
- 2
- 3
- 4
- main func
当去掉 DelayPrint() 函数里的 sleep 之后, 输出为:
- 1
- 2
- 3
- 4
- Hello goroutine
- main function
说明第二个 goroutine 不会因为第一个而堵塞或者等待. 事实是当程序执行 go FUNC()的时候, 只是简单的调用然后就立即返回了, 并不关心函数里头发生的故事情节, 所以不同的 goroutine 直接不影响, main 会继续按顺序执行语句.
goroutine 阻塞
场景一:
- package main
- func main() {
- ch := make(chan int)
- <- ch // 阻塞 main goroutine, 通道被锁
- }
运行程序会报错:
- fatal error: all goroutines are asleep - deadlock!
- goroutine 1 [chan receive]:
- main.main()
场景二
- package main
- func main() {
- ch1, ch2 := make(chan int), make(chan int)
- go func() {
- ch1 <- 1 // ch1 通道的数据没有被其他 goroutine 读取走, 堵塞当前 goroutine
- ch2 <- 0
- }()
- <- ch2 // ch2 等待数据的写
- }
非缓冲通道上如果只有数据流入, 而没有流出, 或者只流出无流入, 都会引起阻塞. goroutine 的非缓冲通道里头一定要一进一出, 成对出现. 上面例子, 一: 流出无流入; 二: 流入无流出.
处理方式:
1. 读取通道数据
- package main
- func main() {
- ch1, ch2 := make(chan int), make(chan int)
- go func() {
- ch1 <- 1 // ch1 通道的数据没有被其他 goroutine 读取走, 堵塞当前 goroutine
- ch2 <- 0
- }()
- <- ch1 // 取走便是
- <- ch2 // chb 等待数据的写
- }
2. 创建缓冲通道
- package main
- func main() {
- ch1, ch2 := make(chan int, 3), make(chan int)
- go func() {
- ch1 <- 1 // cha 通道的数据没有被其他 goroutine 读取走, 堵塞当前 goroutine
- ch2 <- 0
- }()
- <- ch2 // ch2 等待数据的写
- }
2. goroutine 调度器相关结构
goroutine 的调度涉及到几个重要的数据结构, 我们先逐一介绍和分析这几个数据结构. 这些数据结构分别是结构体 G, 结构体 M, 结构体 P, 以及 Sched 结构体. 前三个的定义在文件 runtime/runtime.h 中, 而 Sched 的定义在 runtime/proc.c 中. Go 语言的调度相关实现也是在文件 proc.c 中.
2.1 结构体 G
g 是 goroutine 的缩写, 是 goroutine 的控制结构, 是对 goroutine 的抽象. 看下它内部主要的一些结构:
- type g struct {
- // 堆栈参数.
- // 堆栈描述了实际的堆栈内存:[stack.lo,stack.hi).
- // stackguard0 是在 Go 堆栈增长序言中比较的堆栈指针.
- // 通常是 stack.lo + StackGuard, 但是可以通过 StackPreempt 触发抢占.
- // stackguard1 是在 C 堆栈增长序言中比较的堆栈指针.
- // 它是 g0 和 gsignal 堆栈上的 stack.lo + StackGuard.
- // 在其他 goroutine 堆栈上为0, 以触发对 morestackc 的调用(并崩溃).
- // 当前 g 使用的栈空间, stack 结构包括 [lo, hi]两个成员
- stack stack // offset known to runtime/cgo
- // 用于检测是否需要进行栈扩张, go 代码使用
- stackguard0 uintptr // offset known to liblink
- // 用于检测是否需要进行栈扩展, 原生代码使用的
- stackguard1 uintptr // offset known to liblink
- // 当前 g 所绑定的 m
- m *m // current m; offset known to ARM liblink
- // 当前 g 的调度数据, 当 goroutine 切换时, 保存当前 g 的上下文, 用于恢复
- sched gobuf
- // goroutine 运行的函数
- fnstart *FuncVal
- // g 当前的状态
- atomicstatus uint32
- // 当前 g 的 id
- goid int64
- // 状态 Gidle,Grunnable,Grunning,Gsyscall,Gwaiting,Gdead
- status int16
- // 下一个 g 的地址, 通过 guintptr 结构体的 ptr set 函数可以设置和获取下一个 g, 通过这个字段和 sched.gfreeStack sched.gfreeNoStack 可以把 free g 串成一个链表
- schedlink guintptr
- // 判断 g 是否允许被抢占
- preempt bool // preemption signal, duplicates stackguard0 = stackpreempt
- // g 是否要求要回到这个 M 执行, 有的时候 g 中断了恢复会要求使用原来的 M 执行
- lockedm muintptr
- // 用于传递参数, 睡眠时其它 goroutine 设置 param, 唤醒时此 goroutine 可以获取
- param *void
- // 创建这个 goroutine 的 go 表达式的 pc
- uintptr gopc
- }
其中包含了栈信息 stackbase 和 stackguard, 有运行的函数信息 fnstart. 这些就足够成为一个可执行的单元了, 只要得到 CPU 就可以运行. goroutine 切换时, 上下文信息保存在结构体的 sched 域中. goroutine 切换时, 上下文信息保存在结构体的 sched 域中. goroutine 是轻量级的线程或者称为协程, 切换时并不必陷入到操作系统内核中, 很轻量级.
结构体 G 中的 Gobuf, 其实只保存了当前栈指针, 程序计数器, 以及 goroutine 自身.
- struct Gobuf
- {
- // 这些字段的偏移是 libmach 已知的(硬编码的).
- sp uintper;
- pc *byte;
- g *G;
- ...
- };
记录 g 是为了恢复当前 goroutine 的结构体 G 指针, 运行时库中使用了一个常驻的寄存器 extern register G* g, 这是当前 goroutine 的结构体 G 的指针. 这种结构是为了快速地访问 goroutine 中的信息, 比如, Go 的栈的实现并没有使用 %ebp 寄存器, 不过这可以通过 g->stackbase 快速得到."extern register" 是由 6c,8c 等实现的一个特殊的存储, 在 ARM 上它是实际的寄存器. 在 Linux 系统中, 对 g 和 m 使用的分别是 0(GS)和 4(GS). 链接器还会根据特定操作系统改变编译器的输出, 每个链接到 Go 程序的 C 文件都必须包含 runtime.h 头文件, 这样 C 编译器知道避免使用专用的寄存器.
2.2 结构体 P
P 是 Processor 的缩写. 结构体 P 的加入是为了提高 Go 程序的并发度, 实现更好的调度. M 代表 OS 线程. P 代表 Go 代码执行时需要的资源.
- type p struct {
- lock mutex
- id int32
- // p 的状态, 稍后介绍
- status uint32 // one of pidle/prunning/...
- // 下一个 p 的地址, 可参考 g.schedlink
- link puintptr
- // p 所关联的 m
- m muintptr // back-link to associated m (nil if idle)
- // 内存分配的时候用的, p 所属的 m 的 mcache 用的也是这个
- mcache *mcache
- // Cache of goroutine ids, amortizes accesses to runtime.sched.goidgen.
- // 从 sched 中获取并缓存的 id, 避免每次分配 goid 都从 sched 分配
- goidcache uint64
- goidcacheend uint64
- // Queue of runnable goroutines. Accessed without lock.
- // p 本地的 runnbale 的 goroutine 形成的队列
- runqhead uint32
- runqtail uint32
- runq [256]guintptr
- // runnext, 如果不是 nil, 则是已准备好运行的 G
- // 当前的 G, 并且应该在下一个而不是其中运行
- // runq, 如果运行 G 的时间还剩时间
- // 切片. 它将继承当前时间剩余的时间
- // 切片. 如果一组 goroutine 锁定在
- // 交流等待模式, 该计划将其设置为
- // 单位并消除 (可能很大) 调度
- // 否则会由于添加就绪商品而引起的延迟
- // goroutines 到运行队列的末尾.
- // 下一个执行的 g, 如果是 nil, 则从队列中获取下一个执行的 g
- runnext guintptr
- // Available G's (status == Gdead)
- // 状态为 Gdead 的 g 的列表, 可以进行复用
- gfree *g
- gfreecnt int32
- }
跟 G 不同的是, P 不存在 waiting 状态. MCache 被移到了 P 中, 但是在结构体 M 中也还保留着. 在 P 中有一个 Grunnable 的 goroutine 队列, 这是一个 P 的局部队列. 当 P 执行 Go 代码时, 它会优先从自己的这个局部队列中取, 这时可以不用加锁, 提高了并发度. 如果发现这个队列空了, 则去其它 P 的队列中拿一半过来, 这样实现工作流窃取的调度. 这种情况下是需要给调用器加锁的.
2.3 结构体 M
M 是 machine 的缩写, 是对机器的抽象, 每个 m 都是对应到一条操作系统的物理线程.
- type m struct {
- // g0 是用于调度和执行系统调用的特殊 g
- g0 *g // goroutine with scheduling stack
- // m 当前运行的 g
- curg *g // current running goroutine
- // 当前拥有的 p
- p puintptr // attached p for executing go code (nil if not executing go code)
- // 线程的 local storage
- tls [6]uintptr // thread-local storage
- // 唤醒 m 时, m 会拥有这个 p
- nextp puintptr
- id int64
- // 如果 !="", 继续运行 curg
- preemptoff string // if != "", keep curg running on this m
- // 自旋状态, 用于判断 m 是否工作已结束, 并寻找 g 进行工作
- spinning bool // m is out of work and is actively looking for work
- // 用于判断 m 是否进行休眠状态
- blocked bool // m is blocked on a note
- // m 休眠和唤醒通过这个, note 里面有一个成员 key, 对这个 key 所指向的地址进行值的修改, 进而达到唤醒和休眠的目的
- park note
- // 所有 m 组成的一个链表
- alllink *m // on allm
- // 下一个 m, 通过这个字段和 sched.midle 可以串成一个 m 的空闲链表
- schedlink muintptr
- // mcache,m 拥有 p 的时候, 会把自己的 mcache 给 p
- mcache *mcache
- // lockedm 的对应值
- lockedg guintptr
- // 待释放的 m 的 list, 通过 sched.freem 串成一个链表
- freelink *m // on sched.freem
- }
和 G 类似, M 中也有 alllink 域将所有的 M 放在 allm 链表中. lockedg 是某些情况下, G 锁定在这个 M 中运行而不会切换到其它 M 中去. M 中还有一个 MCache, 是当前 M 的内存的缓存. M 也和 G 一样有一个常驻寄存器变量, 代表当前的 M. 同时存在多个 M, 表示同时存在多个物理线程.
2.4 Sched 结构体
Sched 是调度实现中使用的数据结构, 该结构体的定义在文件 proc.c 中.
- type schedt struct {
- // 全局的 go id 分配
- goidgen uint64
- // 记录的最后一次从 i/o 中查询 g 的时间
- lastpoll uint64
- lock mutex
- // 当增加 nmidle,nmidlelocked,nmsys 或 nmfreed 时, 应
- // 确保调用 checkdead().
- // m 的空闲链表, 结合 m.schedlink 就可以组成一个空闲链表了
- midle muintptr // idle m's waiting for work
- nmidle int32 // number of idle m's waiting for work
- nmidlelocked int32 // number of locked m's waiting for work
- // 下一个 m 的 id, 也用来记录创建的 m 数量
- mnext int64 // number of m's that have been created and next M ID
- // 最多允许的 m 的数量
- maxmcount int32 // maximum number of m's allowed (or die)
- nmsys int32 // number of system m's not counted for deadlock
- // free 掉的 m 的数量, exit 的 m 的数量
- nmfreed int64 // cumulative number of freed m's
- ngsys uint32 // 系统 goroutine 的数量; 原子更新
- pidle puintptr // 闲置的
- npidle uint32
- nmspinning uint32 // See "Worker thread parking/unparking" comment in proc.go.
- // Global runnable queue.
- // 这个就是全局的 g 的队列了, 如果 p 的本地队列没有 g 或者太多, 会跟全局队列进行平衡
- // 根据 runqhead 可以获取队列头的 g, 然后根据 g.schedlink 获取下一个, 从而形成了一个链表
- runqhead guintptr
- runqtail guintptr
- runqsize int32
- // freem 是 m 等待被释放时的列表
- // 设置了 m.exited. 通过 m.freelink 链接.
- // 等待释放的 m 的列表
- freem *m
- }
大多数需要的信息都已放在了结构体 M,G 和 P 中, Sched 结构体只是一个壳. 可以看到, 其中有 M 的 idle 队列, P 的 idle 队列, 以及一个全局的就绪的 G 队列. Sched 结构体中的 Lock 是非常必须的, 如果 M 或 P 等做一些非局部的操作, 它们一般需要先锁住调度器.
3. G,P,M 相关状态
g.status
_Gidle: goroutine 刚刚创建还没有初始化
_Grunnable: goroutine 处于运行队列中, 但是还没有运行, 没有自己的栈
_Grunning: 这个状态的 g 可能处于运行用户代码的过程中, 拥有自己的 m 和 p
_Gsyscall: 运行 systemcall 中
_Gwaiting: 这个状态的 goroutine 正在阻塞中, 类似于等待 channel
_Gdead: 这个状态的 g 没有被使用, 有可能是刚刚退出, 也有可能是正在初始化中
_Gcopystack: 表示 g 当前的栈正在被移除, 新栈分配中
p.status
_Pidle: 空闲状态, 此时 p 不绑定 m
_Prunning: m 获取到 p 的时候, p 的状态就是这个状态了, 然后 m 可以使用这个 p 的资源运行 g
_Psyscall: 当 go 调用原生代码, 原生代码又反过来调用 go 的时候, 使用的 p 就会变成此态
_Pdead: 当运行中, 需要减少 p 的数量时, 被减掉的 p 的状态就是这个了
m.status
m 的 status 没有 p,g 的那么明确, 但是在运行流程的分析中, 主要有以下几个状态
运行中: 拿到 p, 执行 g 的过程中
运行原生代码: 正在执行原声代码或者阻塞的 syscall
休眠中: m 发现无待运行的 g 时, 进入休眠, 并加入到空闲列表中
自旋中(spining): 当前工作结束, 正在寻找下一个待运行的 g
4. G,P,M 的调度关系
一个 G 就是一个 gorountine, 保存了协程的栈, 程序计数器以及它所在 M 的信息. P 全称是 Processor, 处理器, 它的主要用途就是用来执行 goroutine 的. M 代表内核级线程, 一个 M 就是一个线程, goroutine 就是跑在 M 之上的. 程序启动时, 会创建一个主 G, 而每使用一次 go 关键字也创建一个 G.go func()创建一个新的 G 后, 放到 P 的本地队列里, 或者平衡到全局队列, 然后检查是否有可用的 M, 然后唤醒或新建一个 M,M 获取待执行的 G 和空闲的 P, 将调用参数保存到 g 的栈, 将 sp,pc 等上下文环境保存在 g 的 sched 域, 这样整个 goroutine 就准备好了, 只要等分配到 CPU, 它就可以继续运行, 之后再清理现场, 重新进入调度循环.
4.1 调度实现
图中有两个物理线程, M0,M1 每一个 M 都拥有一个处理器 P, 每一个 P 都有一个正在运行的 G.P 的数量可以通过 GOMAXPROCS()来设置, 它其实也代表了真正的并发度, 即有多少个 goroutine 可以同时运行. 图中灰色 goroutine 都是处于 ready 的就绪态, 正在等待被调度. 由 P 维护这个就绪队列(runqueue),go function 每启动一个 goroutine,runqueue 队列就在其末尾加入一个 goroutine, 在下一个调度点, 就从 runqueue 中取出一个 goroutine 执行.
当一个 OS 线程 M0 陷入阻塞时, P 转而在 M1 上运行 G, 图中的 M1 可能是正被创建, 或者从线程缓存中取出. 当 MO 返回时, 它尝试取得一个 P 来运行 goroutine, 一般情况下, 它会从其他的 OS 线程那里拿一个 P 过来执行, 像 M1 获取 P 一样; 如果没有拿到的话, 它就把 goroutine 放在一个 global runqueue(全局运行队列)里, 然后自己睡眠(放入线程缓存里). 所有的 P 会周期性的检查全局队列并运行其中的 goroutine, 否则其上的 goroutine 永远无法执行.
另一种情况是 P 上的任务 G 很快就执行完了(分配不均), 这个处理器 P 很忙, 但是其他的 P 还有任务, 此时如果 global runqueue 也没有 G 了, 那么 P 就会从其他的 P 里拿一些 G 来执行. 一般来说, 如果一般就拿 run queue 的一半, 这就确保了每个 OS 线程都能充分的使用.
golang 采用了 m:n 线程模型, 即 m 个 gorountine(简称为 G)映射到 n 个用户态进程 (简称为 P) 上, 多个 G 对应一个 P, 一个 P 对应一个内核线程(简称为 M).
P 的数量: 由启动时环境变量 $GOMAXPROCS 或者是由 runtime 的方法 GOMAXPROCS()决定(默认是 1). 这意味着在程序执行的任意时刻都只有 $GOMAXPROCS 个 goroutine 在同时运行. 在确定了 P 的最大数量 n 后, 运行时系统会根据这个数量创建 n 个 P.
M 的数量: go 语言本身的限制: go 程序启动时, 会设置 M 的最大数量, 默认 10000. 但是内核很难支持这么多的线程数, 所以这个限制可以忽略. runtime/debug 中的 SetMaxThreads 函数, 设置 M 的最大数量. 一个 M 阻塞了, 会创建新的 M.
M 与 P 的数量没有绝对关系, 一个 M 阻塞, P 就会去创建或者切换另一个 M, 所以, 即使 P 的默认数量是 1, 也有可能会创建很多个 M 出来.
P 上 G 的调度: 如果一个 G 不主动让出 CPU 或被动 block, 所属 P 中的其他 G 会一直等待顺序执行.
一个 G 执行 IO 时可能会进入 waiting 状态, 主动让出 CPU, 此时会被移到所属 P 中的其他 G 后面, 等待下一次轮到执行.
一个 G 调用了 runtime.Gosched()会进入 runnable 状态, 主动让出 CPU, 并被放到全局等待队列中.
一个 G 调用了 runtime.Goexit(), 该 G 将会被立即终止, 然后把已加载的 defer(有点类似析构)依次执行完.
一个 G 调用了允许 block 的 syscall, 此时 G 及其对应的 P, 其他 G 和 M 都会被 block 起来, 监控线程 M 会定时扫描所有 P, 一旦发现某个 P 处于 block syscall 状态, 则通知调度器让另一个 M 来带走 P(这里的另一个 M 可能是新创建的, 因此随着 G 被不断 block,M 数量会不断增加, 最终 M 数量可能会超过 P 数量), 这样 P 及其余下的 G 就不会被 block 了, 等被 block 的 M 返回时发现自己的 P 没有了, 也就不能再处理 G 了, 于是将 G 放入全局等待队列等待空闲 P 接管, 然后 M 自己 sleep.
通过实验, 当一个 G 运行了很久(比如进入死循环), 会被自动切到其他 CPU 核, 可能是因为超过时间片后 G 被移到全局等待队列中, 后面被其他 CPU 核上的 M 处理.
M 上 P 和 G 的调度: 每当一个 G 要开始执行时, 调度器判断当前 M 的数量是否可以很好处理完 G: 如果 M 少 G 多且有空闲 P, 则新建 M 或唤醒一个 sleep M, 并指定使用某个空闲 P; 如果 M 应付得来, G 被负载均衡放入一个现有 P+M 中.
当 M 处理完其身上的所有 G 后, 会再去全局等待队列中找 G, 如果没有就从其他 P 中分一半的 G(以便保证各个 M 处理 G 的负载大致相等), 如果还没有, M 就去 sleep 了, 对应的 P 变为空闲 P.
在 M 进入 sleep 期间, 调度器可能会给其 P 不断放入 G, 等 M 醒后(比如超时): 如果 G 数量不多, 则 M 直接处理这些 G; 如果 M 觉得 G 太多且有空闲 P, 会先主动唤醒其他 sleep 的 M 来分担 G, 如果没有其他 sleep 的 M, 调度器创建新 M 来分担.
协程特点
协程拥有自己的寄存器上下文和栈. 协程调度切换时, 将寄存器上下文和栈保存到其他地方, 在切回来的时候, 恢复先前保存的寄存器上下文和栈. 因此, 协程能保留上一次调用时的状态(即所有局部状态的一个特定组合), 每次过程重入时, 就相当于进入上一次调用的状态, 换种说法: 进入上一次离开时所处逻辑流的位置. 线程和进程的操作是由程序触发系统接口, 最后的执行者是系统; 协程的操作执行者则是用户自身程序, goroutine 也是协程.
来源: https://www.cnblogs.com/33debug/p/11897627.html