golang gmp

183 阅读6分钟

协程快的原因

  • 协程的管理依赖Go语言运行时的调度器,不需要切换线程
  • 线程是有固定的栈的,基本都是2MB go采用了动态扩张收缩的策略:初始化为2KB,最大可扩张到1GB。

gm

图片.png

缺点

(1)创建、销毁、调度G都需要每个M获取锁,这就形成了激烈的锁竞争。(2)M转移G会造成延迟和额外的系统负载。比如当G中包含创建新协程的时候,M创建了G’,为了继续执行G,需要把G’交给M2(假如被分配到)执行,也造成了很差的局部性,因为G’和G是相关的,最好放在M上执行,而不是其他M2

(3)系统调用(CPU在M之间的切换)导致频繁的线程阻塞和取消阻塞操作增加了系统开销。

gmp

图片.png

在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或者是由runtime的方法GOMAXPROCS()决定。这意味着在程序执行的任意时刻都只有GOMAXPROCS或者是由runtime的方法GOMAXPROCS()决定。这意味着在程序执行的任意时刻都只有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会放在全局空间。