🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
## GMP模型调度器 GMP模型调度器主要概念如下: * G(Goroutine): 即Go协程,每个go关键字都会创建一个协程 * M(Machine): 工作线程,在Go中称为Machine * P(Processor): 处理器(Go中定义的一个摡念,不是指CPU),包含运行Go代码的必要资源,也有调度goroutine的能力 ### 调度原理 ![](https://img.kancloud.cn/fa/83/fa837532e207117cb05de03c7fcb18e7_1024x768.png) * 全局队列(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,不断重复下去。 ![](https://img.kancloud.cn/30/11/3011e9a4be14e9ea5e143584cfaf9727_1240x698.png) 调度流程: 1. 我们通过 go func () 来创建一个 goroutine; 2. ​有两个存储 G 的队列,一个是局部调度器 P 的本地队列、一个是全局 G 队列。新创建的 G 会先保存在 P 的本地队列中,如果 P 的本地队列已经满了就会保存在全局的队列中; 3. G 只能运行在 M 中,一个 M 必须持有一个 P,M 与 P 是 1:1 的关系。M 会从 P 的本地队列弹出一个可执行状态的 G 来执行;不考虑G进入系统调用或IO操作的情况下,P周期性的将G调度到M中执行,执行一小段时间,将上下文保存下来,然后将G放到队列尾部,然后从队列中重新取出一个G进行调度;如果 P 的本地队列为空,先查询全局队列,如果全局队列中也没有G,则从其他MP组合偷取一部分可执行的P来执行(一般每次偷取一半); 4. 一个 M 调度 G 执行的过程是一个循环机制; 5. 当 M 执行某一个 G 时候如果发生了 syscall 或则其余阻塞操作,M 会阻塞,M将释放P,进而某个空闲的M1(M1的来源有可能是M的缓存池,也可能是新建的)获取P,继续执行P队列中剩下的G; 当M运行的某个G产生系统调用时,如下图所示: ![](https://img.kancloud.cn/09/b7/09b7780100c33083307f5158f9e79268_550x400.png) 6. 当G0系统调用结束后,根据M0是否能获取到P,将会将G0做不同的处理: a> 如果有空闲的P,则获取一个P,继续执行G0; b> 如果没有空闲的P,则将G0放入全局队列,等待被其他的P调度。然后M0将进入缓存池睡眠 --- *说明:每个P会周期性地查看全局队列中是否有G待运行并将其调度到M中执行,全局队列中G的来源,主要有从系统调用中恢复的G。之所以P会周期性地查看全局队列,也是为了防止全局队列中的G被饿死* ### 调度器的设计策略 复用线程:避免频繁的创建、销毁线程,而是对线程的复用。 1)work stealing 机制 ​ 当本线程无可运行的 G 时,尝试从其他线程绑定的 P 偷取 G,而不是销毁线程。 2)hand off 机制 ​ 当本线程因为 G 进行系统调用阻塞时,线程释放绑定的 P,把 P 转移给其他空闲的线程执行。 利用并行:GOMAXPROCS 设置 P 的数量,最多有 GOMAXPROCS 个线程分布在多个 CPU 上同时运行。GOMAXPROCS 也限制了并发的程度,比如 GOMAXPROCS = 核数/2,则最多利用了一半的 CPU 核进行并行。 抢占:在 coroutine 中要等待一个协程主动让出 CPU 才执行下一个协程,在 Go 中,一个 goroutine 最多占用 CPU 10ms,防止其他 goroutine 被饿死,这就是 goroutine 不同于 coroutine 的一个地方。 全局 G 队列:在新的调度器中依然有全局 G 队列,但功能已经被弱化了,当 M 执行 work stealing 从其他 P 偷不到 G 时,它可以从全局 G 队列获取 G ### Goroutine vs Machine * P的数量由启动时环境变量 $GOMAXPROCS 或者是由 runtime 的方法 GOMAXPROCS() 决定。这意味着在程序执行的任意时刻都只有 $GOMAXPROCS 个 goroutine 在同时运行 * M的数量由go语言本身的限制,go程序启动时,会设置M的最大数量,默认10000. 但是内核很难支持这么多的线程数,所以这个限制可以忽略;runtime/debug中的SetMaxThreads 函数,设置M的最大数量;一般大于等于P的个数 > P vs M 的创建 P何时创建:在确定了P的最大数量n后,运行时系统会根据这个数量创建n个P。 M 何时创建:当没有足够的 M 来关联 P 并运行其中的可运行的 G。比如所有的 M 此时都阻塞住了,而 P 中还有很多就绪任务,就会去寻找空闲的 M,而没有空闲的,就会去创建新的 M GOMAXPROCS设置对性能的影响 一般来讲,程序运行时就将GOMAXPROCS大小设置为CPU核数,可让Go程序充分利用CPU;在某些IO密集型的应用里,这个值可能并不意味着性能最好。 理论上当某个Goroutine进入系统调用时,会有一个新的M被启用或创建,继续占满CPU。 但由于Go调度器检测到M被阻塞是有一定延迟的,也即旧的M被阻塞和新的M得到运行之间是有一定间隔的,所以在IO密集型应用中不妨把GOMAXPROCS设置的大一些,或许会有好的效果