协程快的原因
- 协程的管理依赖Go语言运行时的调度器,不需要切换线程
- 线程是有固定的栈的,基本都是2MB go采用了动态扩张收缩的策略:初始化为2KB,最大可扩张到1GB。
gm
缺点
(1)创建、销毁、调度G都需要每个M获取锁,这就形成了激烈的锁竞争。(2)M转移G会造成延迟和额外的系统负载。比如当G中包含创建新协程的时候,M创建了G’,为了继续执行G,需要把G’交给M2(假如被分配到)执行,也造成了很差的局部性,因为G’和G是相关的,最好放在M上执行,而不是其他M2
(3)系统调用(CPU在M之间的切换)导致频繁的线程阻塞和取消阻塞操作增加了系统开销。
gmp
在GPM模型中有几个重要的概念。
(1)全局队列(Global Queue):存放等待运行的G。全局队列是可能被任意的P去获取里面的G,所以全局队列相当于整个模型中的全局资源,那么自然对于队列的读写操作是要加入互斥动作的。
(2)P的本地队列:同全局队列类似,存放的也是等待运行的G,存的数量有限,不超过256个。新建G'时,G'优先加入到P的本地队列,如果队列满了,则会把本地队列中一半的G移动到全局队列。
(3)P列表:所有的P都在程序启动时创建,并保存在数组中,最多有GOMAXPROCS(可配置)个。
(4)M:线程想运行任务就得获取P,从P的本地队列获取G,P队列为空时,M也会尝试从全局队列拿一批G放到P的本地队列,或从其他P的本地队列偷一半放到自己P的本地队列。M运行G,G执行之后,M会从P获取下一个G,不断重复下去。
Goroutine 调度器和OS 调度器是通过M 结合起来的,每个M 都代表了1 个内核线程,OS 调度器负责把内核线程分配到CPU 的核上执行。
1. 有关P和M个数的问题
(1)P的数量由启动时环境变量GOMAXPROCS个Goroutine在同时运行。(2)M的数量由Go语言本身的限制决定,Go程序启动时会设置M的最大数量,默认为10000,但是内核很难支持这么多的线程数,所以这个限制可以忽略。runtime/debug中的SetMaxThreads()函数可设置M的最大数量,当一个M阻塞了时会创建新的M。M与P的数量没有绝对关系,一个M阻塞,P就会去创建或者切换另一个M,所以,即使P的默认数量是1,也有可能会创建很多个M出来。
2. 有关P和M何时被创建
(1)P创建的时机在确定了P的最大数量n后,运行时系统会根据这个数量创建n个P。(2)M创建的时机是在当没有足够的M来关联P并运行其中可运行的G的时候。比如所有的M此时都阻塞住了,而P中还有很多就绪任务,就会去寻找空闲的M,而没有空闲的,就会去创建新的M。
调度器的设计策略
策略一:复用线程
偷取(Work Stealing)机制
当本线程无可运行的G时,尝试从其他线程绑定的P偷取G,而不是销毁线程
移交(Hand Off)机制
当本线程因为G进行系统调用阻塞时,线程释放绑定的P,把P转移给其他空闲的线程执行。此时若M1的GPM组合中,G1正在被调度,且已经发生了阻塞,则这个时候就会触发移交的设计机制。GPM模型为了更大程度的利用M和P的性能,不会让一个P永远被一个阻塞的G1耽误之后的工作。所以遇见这种情况的时候,移交机制的设计概念是应该立刻将此时的P释放出来。
策略二:利用并行
GOMAXPROCS设置P的数量,最多有GOMAXPROCS个线程分布在多个CPU上同时运行。GOMAXPROCS也限制了并发的程度,比如GOMAXPROCS = ``核数/2,则最多利用了一半的CPU核进行并行。
****策略三:抢占
在Coroutine中要等待一个协程主动让出CPU才执行下一个协程,在Go中,一个Goroutine最多占用CPU 10ms,防止其他Goroutine无资源可用,这就是Goroutine不同于Goroutine的一个地方。
策略四:全局G队列
在新的调度器中依然有全局G队列,但功能已经被弱化了,当M执行偷取,但从其他P偷不到G时,它可以从全局G队列获取G,如图17所示
流程
- 1、我们通过 go func()来创建一个goroutine;
- 2、有两个存储G的队列列,一个是局部调度器器P的本地队列列、一个是全局G队列列。新创建的G会先保存在P的本地队列列中,如果P的本地队列列已经满了了就会保存在全局的队列列中;
- 3、G只能运⾏行行在M中,⼀个M必须持有一个P,M与P是1:1的关系。M会从P的本地队列列弹出一个可执行状态的G来执⾏,如果P的本地队列为空,就会想其他的MP组合偷取一个可执行的G来执⾏
- 4、一个M调度G执行的过程是一个循环机制;
- 5、当M执⾏行行某一个G时候如果发⽣生了了syscall或则其余阻塞操作,M会阻塞,如果当前有一些G在执⾏runtime会把这个线程M从P中摘除(detach),然后再创建⼀一个新的操作系统的线程(如果有空闲的线程可用就复⽤用空闲线程)来服务于这个P;
- 6、当M系统调用结束时候,这个G会尝试获取一个空闲的P执行,并放入到这个P的本地队列列。如果获取不到P,那么这个线程M变成休眠状态, 加⼊入到空闲线程中,然后这个G会被放⼊全局队列列中。
在Golang调度器的GPM模型中还有两个比较特殊的角色,他们分别是M0和G0。
1. M0
(1)启动程序后的编号为0的主线程。
(2)在全局便令runtime.m0中,不需要在heap堆上分配。
(3)负责执行初始化操作和启动第一个G。
(4)启动第一个G后,M0就和其他的M一样了。
2. G0
(1)每次启动一个M,创建的第一个Goroutine,就是G0。
(2)G0是仅用于负责调度的G。
(3)G0不指向任何可执行的函数。
(4)每个M都会有一个自己的G0。
(5)在调度或系统调度时,会使用M切换到G0,再通过G0调度。
(6)M0的G0会放在全局空间。