goroutine的调度问题,一样也是我以前面试的问题,不过这个问题我当时并非很清楚,回来之后立马查阅资料,现整理出来备忘。面试
有一些预备知识须要说明,就是操做系统中的线程。操做系统中的线程分为两种:内核线程和用户线程。用户平时使用的线程并非内核线程,而是存在于用户态的用户线程。用户线程并不必定在操做系统内核中对用同等数量的内核线程。这里有三个模型:缓存
1.一对一模型(1:1)多线程
2. 多对一模型(N:1)并发
3. 多对多模型(N:M)性能
下面就先来谈谈这三种线程模型。spa
1.一对一模型(1:1)操作系统
对于支持线程的操做系统来讲,一对一模型是最简单的一种线程模型了,一个用户线程惟一对应一个内核线程,但反过来却不必定,一个内核线程并不必定有对应的用户线程存在。这样一来,因为一个内核线程至多只对应一个用户线程,线程之间能够作到最大程度的并发,不一样线程之间不会相互影响,好比一个线程阻塞了也不会影响到其余线程的执行。对于多处理器,一对一的线程模型效率更高。可是不少操做系统限制了内核线程的数量,若是采用一对一模型,用户线程的数量也会受到比较大的限制。并且不少操做系统的内核线程在调度时开销较大,这也会影响用户线程的效率。线程
2. 多对一模型(N:1)3d
多对一模型意味着多个用户线程对应一个内核线程,用户线程间的切换由代码控制,由于线程间切换的效率比较高(不用陷入内核区去切换)。不过若是其中一个用户线程阻塞了,则和它对应相同内核线程的那些用户线程也都会阻塞,由于内核线程是被共用的(且是绑定的),此时它没法抽身出来。并且增长处理器个数对于多对一线程模型帮助也不大,毕竟在这种状况下,一个线程阻塞,相关线程也跟着遭殃的事实和处理器个数关系不大。这种模型的好处是线程间切换开销低,且线程数量能够不少。指针
3. 多对多模型(N:M)
多对多线程模型能够说是上面两种模型的结合,也是最复杂的,它把多个用户线程对应到多个内核线程上,且不少时候不是惟一绑定的。所以一个内核线程在一个时间点能够对应0到多个用户线程。且在运行期间,系统能够根据线程执行状况作合理分配。好比用户线程一、用户线程2和用户线程3对应到一个内核线程1,若是用户线程1阻塞了,系统能够调度用户线程2和用户线程3到其余内核线程上去,这是个动态的过程。多对多线程模型的优点是可让系统资源获得比较均衡的使用,用户线程之间互相影响比较小,且在多处理器上表现不错(虽然增长处理器个数对它性能提高可能不如一对一模型那么高),关键是它很灵活。
Golang的goroutine调度和多对多模型密切相关,Golang本身有本身的调度器scheduler。Golang的调度器内部有三个重要结构:M、P和G。
M: 表明内核线程。
G: 表明一个goroutine,它有本身的栈,指令指针和一些基本信息,用于被调度。
P: 表明调度的上下文,是Golang内部的调度器,负责让多个goroutine在一个内核线程上运行,它实现了N:1到N:M。
能够看到在某个时刻,一个M对应到一个P,一个P上有一个正在运行的G(蓝色的G),且这个P上可能还有多个G等待被调度(灰色的G),P维护着这个调度队列(runqueue)。P的数量能够经过GOMAXPROCS()来设置,它其实也就表明了真正的并发度,即有多少个goroutine能够同时运行。不过须要注意,GOMAXPROCS()的最大值是256。当要启动一个goroutine时,只须要用go function(args)便可,一但咱们启动了一个goroutine,就会在runqueue队尾加入这个goroutine,P会负责调度这些goroutine。
那么若是在某个M被阻塞了呢?这时候就是N:M模型的关键之处了,此时P能够被安排到其余M上去执行,因为P内部维护着一些G的信息,这些G都有独立的栈和指令指针这些基本信息,因此能够很方便地直接换到另外一个未被阻塞的M下。
上图描述了这种状况,在左边,G0正在运行,当G0因为系统调用被阻塞时,调度器会建立或者从线程缓存中取得一个线程M1,转投M1。当G0返回时,它必须得到一个P来执行,此时通常是先查看系统中有没有空闲的P,若是有,就得到一个P,用这个P来执行,若是没能得到一个P,这个G0只能暂时放置到一个全局的执行队列(global runqueue)中,它所处的线程M0也就sleep了。系统中的P们会周期性地检查这个队列,取出里面的G来运行。
注:以上部分信息和图片来自The Go scheduler。