Fork me on GitHub

Golang 的调度是啥样的?

问题:Golang 的调度是啥样的?

答案:Go的调度器内部有四个重要的结构:M,G,P,S(Sched)。groutine能拥有强大的并发实现是通过GPM调度模型实现的.
G代表一个groutine协程;
M代表内核级线程,一个M就是一个线程,goroutine就是跑在M之上的;
P全称是Processor,处理器,它的主要用途就是用来执行goroutine的,所以它也维护了一个goroutine队列,里面存储了所有需要它来执行的goroutine
Sched:代表调度器,它维护有存储M和G的队列以及调度器的一些状态信息等。

调度过程:

每一个M都拥有一个处理器P,每一个M也都有一个正在运行的goroutine。
P的数量可以通过GOMAXPROCS()来设置,它其实也就代表了真正的并发度,即有多少个goroutine可以同时运行。然后每个P都拥有一个队列,用来保存准备好等待被执行的goroutine。每次使用go关键字声明时,一个G对象被创建并加入到本地G队列或者全局G队列。当一个OS线程M0陷入阻塞时,P转而在运行M1,M1可能是正被创建,或者从线程缓存中取出。当MO返回时,它必须尝试取得一个P来运行goroutine,一般情况下,它会从其他的OS线程那里拿一个P过来,
如果没有拿到的话,它就把goroutine放在一个global runqueue里,然后自己睡眠(放入线程缓存里)。所有的P也会周期性的检查global runqueue并运行其中的goroutine,否则global runqueue上的goroutine永远无法执行。另一种情况是P所分配的任务G很快就执行完了(分配不均),这就导致了这个处理器P很忙,但是其他的P还有任务,此时如果global runqueue没有任务G了,那么P不得不从其他的P里拿一些G来执行。一般来说,如果P从其他的P那里要拿任务的话,一般就拿run queue的一半,这就确保了每个OS线程都能充分的使用。

三种调度点:

1.调用runtime.gosched函数。goroutine主动放弃CPU,该goroutine会被置为runnable状态,然后放入全局G队列,P继续执行下一个goroutine。

  1. 主动行为
  2. 使用场景:一般发生在执行长任务又想其他goroutine得到执行机会时调用。

2.调用runtime.park函数。goroutine进入wait状态,除非对其调用runtime.ready函数,否则该goroutine永远不刽得到执行。而P继续执行下一个G任务。

  1. 使用场景:读写channel。一般是在某个条件如果得不到满足就不能继续运行下去时调用,当条件满足后需要使用runtime.ready唤醒它,类似于Java里的await和notify

3.慢系统调用。将该处理器P上设置为syscall状态,然后对应的线程M进入系统调用阻塞等待。sysmom监控线程会定期扫描所有的P(处理器),若发现一个P处于syscall状态,就讲=将M(线程)和P(处理器)分离,并再分配一个M与这个P绑定,从而继续执行P本地队列中的其他goroutine。当之前阻塞的M从系统调用返回后,将其执行的goroutine放入全局G队列中,该M去sleep。

sysmom线程的执行过程:

  1. 记录每一个P(处理器)执行的goroutine数schedtick,每执行一个goroutine后schedtick递增
  2. 如果检查到schedtick一直没有递增,就说明这个P一直在执行同一个goroutine,如果超过一定的时间阈值(10ms),就在这个goroutine任务的栈信息里面加一个标记
  3. 然后这个goroutine任务在执行的时候,如果遇到非内联函数调用,就会检查一次这个标记,然后中断自己,把自己加到队列末尾,执行下一个G(即所谓的调度点3)
  4. 如果没有遇到非内联函数调用(内联函数:执行权不转交给被调用函数,而是将函数的代码粘贴在调用处),那么就会一直执行这个goroutine任务,直到他自己结束。
2020-10-10 16:58:01  LeeChan 阅读(34) 评论(0) 标签:Go 分类:面试