一、golang调度器的由来
1、单进程时代(单任务系统)
顺序执行任务,同一时刻只有一个进程被执行;
进程阻塞带来cpu资源的浪费。
2、多进程多线程时代(多任务系统)
任务并发执行,轮询调度,每个进程执行一个时间片。
上下文切换的切换成本增大,cpu利用率降低。
多线程随着同步竞争(锁、竞争资源冲突)开发设计变得越来越复杂。
占用内存资源大,进程占用内存,虚拟内存4GB(32bit操作系统),线程约4MB。
3、协程时代(内核线程+用户线程)
通过语言级别的协程调度器,调度较多的用户线程,来协调绑定较少的内核线程,减少了内核线程的切换开销。
早期调度器只有一个全局队列,多个m从同一个全局队列竞争g,造成激烈的锁竞争。新创建的g被放入全局队列导致被其他m执行,破坏了局部性。
二、GMP模型
1、GMP 模型
- Processor
它包含了运行 goroutine 的资源,如果线程想运行 goroutine,必须先获取 P,P 中还包含了可运行的 G 队列。
- 全局队列(Global Queue)
存放等待运行的 G。
- P本地队列
存放的也是等待运行的 G,不超过 256 个。新建 G’时,G’优先加入到 P 的本地队列,如果队列满了,则会把本地队列中一半的 G 移动到全局队列。
从 P 本地可运行队列先选出一个可运行的 goroutine;为了公平,调度器每调度 61 次的时候,都会尝试从全局队列里取出待运行的 goroutine 来运行,调用 globrunqget;如果还没找到,就要去其他 P 里面去偷一些 goroutine 来执行
- P 列表
所有的 P 都在程序启动时创建,并保存在数组中,最多有 GOMAXPROCS(可配置) 个。
- M
线程想运行任务就得获取 P,从 P 的本地队列获取 G,P 队列为空时,M 也会尝试从全局队列拿一批 G 放到 P 的本地队列,或从其他 P 的本地队列偷一半放到自己 P 的本地队列。M 运行 G,G 执行之后,M 会从 P 获取下一个 G,不断重复下去。
Goroutine 调度器和 OS 调度器是通过 M 结合起来的,每个 M 都代表了 1 个内核线程,OS 调度器负责把内核线程分配到 CPU 的核上执行。
2、有关 P 和 M 的个数问题
- P 的数量:
由启动时环境变量 $GOMAXPROCS 或者是由 runtime 的方法 GOMAXPROCS() 决定。这意味着在程序执行的任意时刻都只有 $GOMAXPROCS 个 goroutine 在同时运行。
- M 的数量
go 语言本身的限制:go 程序启动时,会设置 M 的最大数量,默认 10000. 但是内核很难支持这么多的线程数,所以这个限制可以忽略。
runtime/debug 中的 SetMaxThreads 函数,设置 M 的最大数量
一个 M 阻塞了,会创建新的 M。
M 与 P 的数量没有绝对关系,一个 M 阻塞,P 就会去创建或者切换另一个 M,所以,即使 P 的默认数量是 1,也有可能会创建很多个 M 出来。
3、P 和 M 何时会被创建
- P 何时创建
在确定了 P 的最大数量 n 后,运行时系统会根据这个数量创建 n 个 P。
- M 何时创建
没有足够的 M 来关联 P 并运行其中的可运行的 G。比如所有的 M 此时都阻塞住了,而 P 中还有很多就绪任务,就会去寻找空闲的 M,而没有空闲的,就会去创建新的 M。
4、sysmon后台监控线程做了什么
到 p 对应的 m 处于系统调用之中到现在已经超过 10 毫秒。这说明系统调用所花费的时间较长,需要对其进行抢占,以此来使得 retake 函数返回值不为 0,这样,会保持 sysmon 线程 20 us 的检查周期,提高 sysmon 监控的实时性。
三、gmp模型的设计思想
1、调度器的设计策略
并行、抢占、复用线程(偷取 + 分离)、减少全局竞争
- 并行
采用M:N模型,通过多核多线程来实现并行。通过GOMAXPROCS 限定P的个数,默认为逻辑cpu数量。
- 抢占
限定G与M的绑定时间,最多10ms,超过时间解绑,避免其他协程饥饿。
- 复用线程(偷取 + 分离)
work stealing 机制(PM偷G):自旋线程(有P无G的M,只有G0),尝试从与其他M绑定的P队列中取队尾一半。避免浪费M资源。
hand off 机制(PM分离):M阻塞,P与M分离,创建/唤醒一个休眠线程接管P,避免其他协程饥饿。
- 减少全局竞争
优先从本地队列调度G,而不是全局队列,减少全局竞争。
2、go func 的调度过程
创建:创建一个G对象
放置:优先放入创建G的线程的本地P队列中,如果本地队列已满,则将当前队列的前半部分和新创建的G一起放入全局队列中。
唤醒:顺便唤醒一个M(如果有空闲P,没有自旋线程,且当前创建者不是main),如果一个M被唤醒,则尝试和一个空闲的新P进行绑定,如果没有空闲P队列,M回到休眠状态。
获取:M尝试从本地P队列中获取G,如果本地P队列中没有G,尝试从全局P队列批量获取G放入本地P队列,或者从其他P队列中批量偷取后一半G放入本地P队列。取不到则M成为自旋线程,自旋超时则成为休眠线程,休眠超时则被GC回收销毁。
调度:调度G,执行,时间片超时或执行结束返回,G被放回本地队列或销毁,M调度执行下一个G。
系统调用或阻塞:如果执行G时发生系统调用(syscall)或者阻塞(锁、channel),如果有其他G在执行,从休眠的M队列唤醒一个M,或者创建一个新的M,接管当前M的P和P队列。
系统调用退出或阻塞返回:当阻塞结束,M被放入休眠M队列,G被放入全局P队列。
3、M0与G0
M0:启动程序后编号为0的主线程,保存在全局变量runtime.m0中,不需要再heap上分配,负责初始化和启动第一个G,启动一个G之后,M0就和其他M一样了
G0:启动M时创建的第一个G,负责调度G,G0不指向任何可执行的函数,每个M都会有一个自己的G0,在调度和系统调用时,M会先切换到G0,然后通过G0切换到其他G,来调度M0的G0会放在全局空间。
四、gmp调度场景分析
1、创建G
新创建的G优先加入本地队列,保证局部性。
2、创建过多的G
本地放满后,将当前队列的前半部分和新创建的G一起打乱放入全局队列中。
3、创建G时,唤醒正在休眠的M
在创建一个G的时候,尝试从休眠的M队列中唤醒一个M。如果一个M被唤醒,新M尝试和一个空闲的新P进行绑定,如果没有空闲P队列,M回到休眠状态。
如果新唤醒的M与空闲的P绑定成功,则调度G0,从本地队列获取G,如果本地队列为空,此时M为自旋线程(本地无G但为运行状态,不断寻找G)。
4、自旋线程M优先从全局队列批量获取G
优先从全局队列获取G,n = min(len(GlobalQueue) / GOMAXPROCS, len(GlobalQueue) / 2),称为从全局队列到本地队列的负载均衡。
5、自旋线程从其他队列批量偷取G
自旋线程优先从全局队列获取G,全局队列为空时,尝试从其他队列偷取后一半。
6、M获取执行G
M优先从本地队列获取G,并且切换G时,通过G0来调度(先切换到G0,再切换到其他G。G0负责调度时协程的切换,函数sehedule),实现线程的复用
7、自旋线程的最大限制
自旋线程 + 执行线程 <= GOMAXPROCS
多余的线程应该放入休眠线程队列。
8、G发生系统调用或者阻塞
G发生系统调用或者阻塞时,P和M分离,从休眠的M队列唤醒一个M,或者创建一个新的M,接管当前M的P和P队列。
9、退出系统调用或阻塞结束
退出系统调用或阻塞结束,M尝试抢占原先的P,抢占失败,尝试从空闲P队列获取P,如果都没有,G会被放入全局队列,M会被放入休眠队列(长期休眠等待GC回收销毁)。
五、调试
1、go tool trace
(1)编写测试代码
package main
import (
"fmt"
"os"
"runtime/trace"
)
/*
go run trace.go
go tool trace trace.out
*/
func main() {
//1. 创建一个trace文件
f, err := os.Create("trace.out")
if err != nil {
panic(err)
}
defer f.Close()
//2. 启动trace
err = trace.Start(f)
if err != nil {
panic(err)
}
//3. 执行程序
fmt.Println("hello gmp trace")
//4. 关闭trace
trace.Stop()
}
(2)运行 go 文件
go run trace.go
(3)解析 trace 文件
go tool trace .\trace.out
Parsing trace...
Splitting trace...
Opening browser. Trace viewer is listening on http://127.0.0.1:6066
Trace Viewer is running with WebComponentsV0 polyfill, and some features may be broken. As a workaround, you may try running chrome with "--enable-blink-features=ShadowDOMV0,CustomElementsV0,HTMLImports" flag. See crbug.com/1036492.
配置环境变量,然后加启动参数,通过终端启动chrome:
chrome --enable-blink-features=ShadowDOMV0,CustomElementsV0,HTMLImports
(4)参数
-
View trace:查看跟踪,能看到一段时间内 goroutine 的调度执行情况,包括事件触发链;
-
Goroutine analysis:Goroutine 分析,能看到这段时间所有 goroutine 执行的一个情况,执行堆栈,执行时间;
-
Network blocking profile:网络阻塞概况(分析网络的一些消耗)
-
Synchronization blocking profile:同步阻塞概况(分析同步锁的一些情况)
-
Syscall blocking profile:系统调用阻塞概况(分析系统调用的消耗)
-
Scheduler latency profile:调度延迟概况(函数的延迟占比)
-
User defined tasks:自定义任务
-
User defined regions:自定义区域
-
Minimum mutator utilization:Mutator 利用率使用情况
2、GODEBUG
GODEBUG=schedtrace=1000 ./trace.out