Go

go调度器

go学习记录

Posted by CHuiL on May 12, 2019

Go调度器有两大思想

复用线程:协程本身就是允许在一些线程上面的,不需要频繁的创建、销毁线程,而是对线程的复用。在调度器中有两个具体的体现
1)work stealing:当本地线程可运行g队列为空时,尝试从其他线程绑定的P偷取G,而不是销毁线程。
2)hand off:当本地线程因为G进行系统调用阻塞时,线程释放绑定的P,把P转移给其他空闲的线程执行。

利用并行:GOMAXPROCS设置P的数量,当该值大于1时,就最多有GOMAXPROCS个线程处于运行状态,这些线程可能分布在不同的CPU核上同时运行,使得并发利用并行。另外GOMAXPROCS也限制了并发的程度,比如GOMAXPROCS=核数/2,则最多利用了一半的CPU核进行并行。

Go调度器的两小策略

抢占:在coroutine中要等待一个协程主动让出CPU才执行下一个协程,在Go中,一个goroutine最多占用CPU 10ms,防止其他goroutine被饿死,这就是goroutine不同于coroutine的一个地方。

全局G队列:当M执行work stealing从其他P偷不到G时,它可以从全局G队列获取G。

Go调度器与OS调度器之间的关系

调度器的4个部分
1.全局队列

存放全局等待运行的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的核上执行。

调度器的生命周期

image

创建G的情况

  • 创建G时,会优先加入到本地P队列;
  • 创建G时,本地P队列满了,为了G的负载均衡,会把本地p队列的前一半G和新的G移到全局G队列(如果新创建的G是紧接着要执行的,那么会被保存在本地p队列中,用某个老的g替换进入全局队列)

运行G执行系统调用阻塞的情况

  • g进行阻塞的系统调用

    1)如果为同步的系统调用,如读取文件,运行g的m和p立即解绑。P中若有g等待运行,或全局队列中有g等待运行,那么会立即唤醒(创建)一个M来与p绑定运行g。否则p会进入空闲p列表等待m来获取p。在系统调用返回后,g会进入本地p队列,而M则会进入空闲m列表等待以后使用。

    2)如果为异步的系统调用,如网络调用,异步系统调用有专门的线程处理,不会使当前的m阻塞,所以m和p不解绑,,只是将g分离出去,在获取可运行g来执行,一旦原来的g返回后,将加入到本地p队列中。

  • g进行非阻塞的同步系统调用,

    m和p会解绑,但是m会记住p,当g从系统调用返回时,m会尝试获取原来的p,如果无法获取则会获取空闲p,如果没有空闲p,则g会进入全局队列。

运行G被lock或channel阻塞的情况

  • 该g会被放入wait队列,M寻找下一个可运行的G。

运行G时的抢占调度

  • 当运行时间超过10ms时
  • 系统调用阻塞超过10us时
    以上情况会触发go中的调度器发送调度信号,让其主动让出p;

G运行完成的情况

  • G会被放入可用g队列中,等待复用,m会运行g0,g0负责调度协程的切换,获取可运行g并切换,使线程复用。

    p中没有可运行G的情况

  • 当前m与p已绑定,p中没有G可运行,会尝试先从全局队列获取一批g,获取的数量符合下面这个公式
    n = min(len(GQ)/GOMAXPROCS + 1, len(GQ)/2),根据这个公式可以保证至少从全局中获取一个g,但不要每次都移动太多,被其他p留点,实现p之间的负载均衡。
  • 全局队列中没有g,则会执行work stealing,随机选择一个p偷取一半g过来。

M的自旋

  • 全局g队列为空,其他p队列也为空,当前p队列为空,并且m没有g执行,则会进行自旋状态,即不断的寻找可以执行的g。
  • 唤醒了m,m与p绑定后,p本地没有g可运行,会进入自旋状态,寻找g来运行,一旦找到g便结束自旋状态
  • 目的:一般自旋的线程要小于不空闲p个数的一半,使没有g运行的m也能保持繁忙状态而不被系统内核切换掉,这样当有一个g需要运行时可以马上被m获取并运行,而不需进行唤醒m的工作,提高执行效率。

    M与G锁定的情况

  • G执行系统调用阻塞时,M与G会锁定。
  • 当某一个M寻找到一个可运行的g,却发现他已经与其他M锁定,那么当前M会让出当前P,并且唤醒该M。而自身则阻塞进入空闲M队列

g0

每个m在创建的时候都会创建一个新的g作为该m的g0,g0负责调度协程的切换,获取可运行g并切换,使线程复用。

参考

从pmg角度看调度