🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
Goroutine协程: 协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。 因此,协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。 线程和进程的操作是由程序触发系统接口,最后的执行者是系统;协程的操作执行者则是用户自身程序,goroutine也是协程。 groutine能拥有强大的并发实现是通过GPM调度模型实现. Go的调度器内部有四个重要的结构:M,P,S,Sched,如上图所示(Sched未给出). * M: M代表内核级线程,一个M就是一个线程,goroutine就是跑在M之上的;M是一个很大的结构,里面维护小对象内存cache(mcache)、当前执行的goroutine、随机数发生器等等非常多的信息. * G: 代表一个goroutine,它有自己的栈,instruction pointer和其他信息(正在等待的channel等等),用于调度. * P: P全称是Processor,逻辑处理器,它的主要用途就是用来执行goroutine的,所以它也维护了一个goroutine队列,里面存储了所有需要它来执行的goroutine. * Sched:代表调度器,它维护有存储M和G的队列以及调度器的一些状态信息等. Go中的GPM调度: 新创建的G 会先保存在 P 的本地队列中,如果 P 的本地队列已经满了就会保存在全局的队列中,最终等待被逻辑处理器P执行即可。 在M与P绑定后,M会不断从P的Local队列中无锁地取出G,并切换到G的堆栈执行,当P的Local队列中没有G时,再从Global队列中获取一个G,当Global队列中也没有待运行的G时,则尝试从其它的P窃取部分G来执行相当于P之间的负载均衡。 [![](https://github.com/KeKe-Li/data-structures-questions/raw/master/src/images/65.jpg)](https://github.com/KeKe-Li/data-structures-questions/blob/master/src/images/65.jpg) 从上图中可以看到,有2个物理线程M,每一个M都拥有一个处理器P,每一个也都有一个正在运行的goroutine。P的数量可以通过GOMAXPROCS()来设置,它其实也就代表了真正的并发度,即有多少个goroutine可以同时运行。 图中灰色的那些goroutine并没有运行,而是出于ready的就绪态,正在等待被调度。P维护着这个队列(称之为runqueue),Go语言里,启动一个goroutine很容易:go function 就行,所以每有一个go语句被执行,runqueue队列就在其末尾加入一个goroutine,在下一个调度点,就从runqueue中取出(如何决定取哪个goroutine?)一个goroutine执行。 当一个OS线程M0陷入阻塞时,P转而在运行M1,图中的M1可能是正被创建,或者从线程缓存中取出。 [![](https://github.com/KeKe-Li/data-structures-questions/raw/master/src/images/60.jpg)](https://github.com/KeKe-Li/data-structures-questions/blob/master/src/images/60.jpg) 当M0返回时,它必须尝试取得一个P来运行goroutine,一般情况下,它会从其他的OS线程那里拿一个P过来,如果没有拿到的话,它就把goroutine放在一个`global runqueue`里,然后自己睡眠(放入线程缓存里)。所有的P也会周期性的检查`global runqueue`并运行其中的goroutine,否则`global runqueue`上的goroutine永远无法执行。 另一种情况是P所分配的任务G很快就执行完了(分配不均),这就导致了这个处理器P处于空闲的状态,但是此时其他的P还有任务,此时如果global runqueue没有任务G了,那么这个P就会从其他的P里偷取一些G来执行。 [![](https://github.com/KeKe-Li/data-structures-questions/raw/master/src/images/64.jpg)](https://github.com/KeKe-Li/data-structures-questions/blob/master/src/images/64.jpg) 通常来说,如果P从其他的P那里要拿任务的话,一般就拿`run queue`的一半,这就确保了每个OS线程都能充分的使用。 [![](https://github.com/KeKe-Li/data-structures-questions/raw/master/src/images/129.jpg)](https://github.com/KeKe-Li/data-structures-questions/blob/master/src/images/129.jpg)