下面由golang教程栏目给大家介绍golang 协程调度 ,希望对需要的朋友有所帮助!
一、线程模型n:1模型,n个用户空间线程在1个内核空间线程上运行。优势是上下文切换非常快但是无法利用多核系统的优点。1:1模型,1个内核空间线程运行一个用户空间线程。这种充分利用了多核系统的优势但是上下文切换非常慢,因为每一次调度都会在用户态和内核态之间切换。(posix线程模型(pthread),java)m:n模型, 每个用户线程对应多个内核空间线程,同时也可以一个内核空间线程对应多个用户空间线程。go打算采用这种模型,使用任意个内核模型管理任意个goroutine。这样结合了以上两种模型的优点,但缺点就是调度的复杂性。
下面看看golang的协程调度
m:一个用户空间线程,同时对应一个内核线程,类似posix pthreadp:代表运行的上下文环境, 也就是我们上一节实现的调度器,一个调度器也会对应一个就绪队列g:goroutine,即协程二、调度模型简介
groutine能拥有强大的并发实现是通过gpm调度模型实现,下面就来解释下goroutine的调度模型。
go的调度器内部有三个重要的结构:m,p,g
m:m是对内核级线程的封装,数量对应真实的cpu数,一个m就是一个线程,goroutine就是跑在m之上的;m是一个很大的结构,里面维护小对象内存cache(mcache)、当前执行的goroutine、随机数发生器等等非常多的信息
g:代表一个goroutine,它有自己的栈,instruction pointer和其他信息(正在等待的channel等等),用于调度。
p:p全称是processor,处理器,它的主要用途就是用来执行goroutine的。每个processor对象都拥有一个lrq(local run queue),未分配的goroutine对象保存在grq(global run queue )中,等待分配给某一个p的lrq中,每个lrq里面包含若干个用户创建的goroutine对象。
golang采用的是多线程模型,更详细的说他是一个两级线程模型,但它对系统线程(内核级线程)进行了封装,暴露了一个轻量级的协程goroutine(用户级线程)供用户使用,而用户级线程到内核级线程的调度由golang的runtime负责,调度逻辑对外透明。goroutine的优势在于上下文切换在完全用户态进行,无需像线程一样频繁在用户态与内核态之间切换,节约了资源消耗。
调度实现
从上图中看,有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可能是正被创建,或者从线程缓存中取出。
当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线程都能充分的使用,如下图:
三、gpm创建相关问题m和p的数量如何确定?或者说何时会创建m和p?
1、p的数量:
由启动时环境变量$gomaxprocs或者是由runtime的方法gomaxprocs()决定(默认是1)。这意味着在程序执行的任意时刻都只有$gomaxprocs个goroutine在同时运行。
2、m的数量:
go语言本身的限制:go程序启动时,会设置m的最大数量,默认10000.但是内核很难支持这么多的线程数,所以这个限制可以忽略。runtime/debug中的setmaxthreads函数,设置m的最大数量一个m阻塞了,会创建新的m。
m与p的数量没有绝对关系,一个m阻塞,p就会去创建或者切换另一个m,所以,即使p的默认数量是1,也有可能会创建很多个m出来。
3、p何时创建:在确定了p的最大数量n后,运行时系统会根据这个数量创建n个p。
4、m何时创建:没有足够的m来关联p并运行其中的可运行的g。比如所有的m此时都阻塞住了,而p中还有很多就绪任务,就会去寻找空闲的m,而没有空闲的,就会去创建新的m。
m选择哪一个p关联?m会选择导致此m被创建的那个p关联。什么时候会切换p与m的关联关系?
当m因系统调用而阻塞时(m上运行的g进入了系统调用的时候),m与p会分开,如果此时p的就绪队列中还有任务,
p就会去关联一个空闲的m,或者创建一个m进行关联。(也就是说go不是像libtask一样处理io阻塞的?不确定。)
就绪的g如何选择进入哪个p的就绪队列?默认情况下:因为p的默认数量是1(m不一定是1),所以如果我们不改变gomaxprocs,无论我们在程序中用go语句创建多少个goroutine,它们都只会被塞入同一个p的就绪队列中。有多个p的情况下:如果修改了gomaxprocs或者调用了runtime.gomaxprocs,运行时系统会把所有的g均匀的分布在各个p的就绪队列中。如何保证每个p的就绪队列中都会有g
如果一个p的就绪队列所有任务都执行完了,那么p会尝试从其他p的就绪队列中取出一部分到自己的就绪队列中,以保证每个p的就绪队列都有任务可以执行。