Goroutine是Go语言原生支持并发的具体实现,你的Go代码都无一例外地跑在goroutine中。你能够启动许多甚至成千上万的goroutine,Go的runtime负责对goroutine进行管理。所谓的管理就是“调度”,粗糙地说调度就是决定什么时候哪一个goroutine将得到资源开始执行、哪一个goroutine应该中止执行让出资源、哪一个goroutine应该被唤醒恢复执行等。goroutine的调度是Go team care的事情,大多数gopher们无需关心。但我的以为适当了解一下Goroutine的调度模型和原理,对于编写出更好的go代码是大有裨益的。所以,在这篇文章中,我将和你们一块儿来探究一下goroutine调度器的演化以及模型/原理。nginx
注意:这里要写的并非对goroutine调度器的源码分析,国内的雨痕老师在其《Go语言学习笔记》一书的下卷“源码剖析”中已经对Go 1.5.1的scheduler实现作了细致且高质量的源码分析了,对Go scheduler的实现特别感兴趣的gopher能够移步到这本书中去^0^。这里关于goroutine scheduler的介绍主要是参考了Go team有关scheduler的各类design doc、国外Gopher发表的有关scheduler的资料,固然雨痕老师的书也给我了不少的启示。git
1、Goroutine调度器
提到“调度”,咱们首先想到的就是操做系统对进程、线程的调度。操做系统调度器会将系统中的多个线程按照必定算法调度到物理CPU上去运行。传统的编程语言好比C、C++等的并发实现实际上就是基于操做系统调度的,即程序负责建立线程(通常经过pthread等lib调用实现),操做系统负责调度。这种传统支持并发的方式有诸多不足:程序员
-
复杂github
- 建立容易,退出难:作过C/C++ Programming的童鞋都知道,建立一个thread(好比利用pthread)虽然参数也很多,但好歹能够接受。但一旦涉及到thread的退出,就要考虑thread是detached,仍是须要parent thread去join?是否须要在thread中设置cancel point,以保证join时能顺利退出?
- 并发单元间通讯困难,易错:多个thread之间的通讯虽然有多种机制可选,但用起来是至关复杂;而且一旦涉及到shared memory,就会用到各类lock,死锁便成为屡见不鲜;
- thread stack size的设定:是使用默认的,仍是设置的大一些,或者小一些呢?
-
难于scalinggolang
为此,Go采用了用户层轻量级thread或者说是类coroutine的概念来解决这些问题,Go将之称为”goroutine“。goroutine占用的资源很是小(Go 1.4将每一个goroutine stack的size默认设置为2k),goroutine调度的切换也不用陷入(trap)操做系统内核层完成,代价很低。所以,一个Go程序中能够建立成千上万个并发的goroutine。全部的Go代码都在goroutine中执行,哪怕是go的runtime也不例外。将这些goroutines按照必定算法放到“CPU”上执行的程序就称为goroutine调度器或goroutine scheduler。web
不过,一个Go程序对于操做系统来讲只是一个用户层程序,对于操做系统而言,它的眼中只有thread,它甚至不知道有什么叫Goroutine的东西的存在。goroutine的调度全要靠Go本身完成,实现Go程序内goroutine之间“公平”的竞争“CPU”资源,这个任务就落到了Go runtime头上,要知道在一个Go程序中,除了用户代码,剩下的就是go runtime了。算法
因而Goroutine的调度问题就演变为go runtime如何将程序内的众多goroutine按照必定算法调度到“CPU”资源上运行了。在操做系统层面,Thread竞争的“CPU”资源是真实的物理CPU,但在Go程序层面,各个Goroutine要竞争的”CPU”资源是什么呢?Go程序是用户层程序,它自己总体是运行在一个或多个操做系统线程上的,所以goroutine们要竞争的所谓“CPU”资源就是操做系统线程。这样Go scheduler的任务就明确了:将goroutines按照必定算法放到不一样的操做系统线程中去执行。这种在语言层面自带调度器的,咱们称之为原生支持并发。编程
2、Go调度器模型与演化过程
一、G-M模型
2012年3月28日,Go 1.0正式发布。在这个版本中,Go team实现了一个简单的调度器。在这个调度器中,每一个goroutine对应于runtime中的一个抽象结构:G,而os thread做为“物理CPU”的存在而被抽象为一个结构:M(machine)。这个结构虽然简单,可是却存在着许多问题。前Intel blackbelt工程师、现Google工程师Dmitry Vyukov在其《Scalable Go Scheduler Design》一文中指出了G-M模型的一个重要不足: 限制了Go并发程序的伸缩性,尤为是对那些有高吞吐或并行计算需求的服务程序。主要体如今以下几个方面:c#
- 单一全局互斥锁(Sched.Lock)和集中状态存储的存在致使全部goroutine相关操做,好比:建立、从新调度等都要上锁;
- goroutine传递问题:M常常在M之间传递”可运行”的goroutine,这致使调度延迟增大以及额外的性能损耗;
- 每一个M作内存缓存,致使内存占用太高,数据局部性较差;
- 因为syscall调用而造成的剧烈的worker thread阻塞和解除阻塞,致使额外的性能损耗。
二、G-P-M模型
因而Dmitry Vyukov亲自操刀改进Go scheduler,在Go 1.1中实现了G-P-M调度模型和work stealing算法,这个模型一直沿用至今:
有名人曾说过:“计算机科学领域的任何问题均可以经过增长一个间接的中间层来解决”,我以为Dmitry Vyukov的G-P-M模型恰是这一理论的践行者。Dmitry Vyukov经过向G-M模型中增长了一个P,实现了Go scheduler的scalable。
P是一个“逻辑Proccessor”,每一个G要想真正运行起来,首先须要被分配一个P(进入到P的local runq中,这里暂忽略global runq那个环节)。对于G来讲,P就是运行它的“CPU”,能够说:G的眼里只有P。但从Go scheduler视角来看,真正的“CPU”是M,只有将P和M绑定才能让P的runq中G得以真实运行起来。这样的P与M的关系,就比如Linux操做系统调度层面用户线程(user thread)与核心线程(kernel thread)的对应关系那样(N x M)。
三、抢占式调度
G-P-M模型的实现算是Go scheduler的一大进步,但Scheduler仍然有一个头疼的问题,那就是不支持抢占式调度,致使一旦某个G中出现死循环或永久循环的代码逻辑,那么G将永久占用分配给它的P和M,位于同一个P中的其余G将得不到调度,出现“饿死”的状况。更为严重的是,当只有一个P时(GOMAXPROCS=1)时,整个Go程序中的其余G都将“饿死”。因而Dmitry Vyukov又提出了《Go Preemptive Scheduler Design》并在Go 1.2中实现了“抢占式”调度。
这个抢占式调度的原理则是在每一个函数或方法的入口,加上一段额外的代码,让runtime有机会检查是否须要执行抢占调度。这种解决方案只能说局部解决了“饿死”问题,对于没有函数调用,纯算法循环计算的G,scheduler依然没法抢占。
四、NUMA调度模型
从Go 1.2之后,Go彷佛将重点放在了对GC的低延迟的优化上了,对scheduler的优化和改进彷佛不那么热心了,只是伴随着GC的改进而做了些小的改动。Dmitry Vyukov在2014年9月提出了一个新的proposal design doc:《NUMA‐aware scheduler for Go》,做为将来Go scheduler演进方向的一个提议,不过至今彷佛这个proposal也没有列入开发计划。
五、其余优化
Go runtime已经实现了netpoller,这使得即使G发起网络I/O操做也不会致使M被阻塞(仅阻塞G),从而不会致使大量M被建立出来。可是对于regular file的I/O操做一旦阻塞,那么M将进入sleep状态,等待I/O返回后被唤醒;这种状况下P将与sleep的M分离,再选择一个idle的M。若是此时没有idle的M,则会新建立一个M,这就是为什么大量I/O操做致使大量Thread被建立的缘由。
Ian Lance Taylor在Go 1.9 dev周期中增长了一个Poller for os package的功能,这个功能能够像netpoller那样,在G操做支持pollable的fd时,仅阻塞G,而不阻塞M。不过该功能依然不能对regular file有效,regular file不是pollable的。不过,对于scheduler而言,这也算是一个进步了。
3、Go调度器原理的进一步理解
一、G、P、M
关于G、P、M的定义,你们能够参见$GOROOT/src/runtime/runtime2.go这个源文件。这三个struct都是大块儿头,每一个struct定义都包含十几个甚至2、三十个字段。像scheduler这样的核心代码向来很复杂,考虑的因素也很是多,代码“耦合”成一坨。不过从复杂的代码中,咱们依然能够看出来G、P、M的各自大体用途(固然雨痕老师的源码分析功不可没),这里简要说明一下:
- G: 表示goroutine,存储了goroutine的执行stack信息、goroutine状态以及goroutine的任务函数等;另外G对象是能够重用的。
- P: 表示逻辑processor,P的数量决定了系统内最大可并行的G的数量(前提:系统的物理cpu核数>=P的数量);P的最大做用仍是其拥有的各类G对象队列、链表、一些cache和状态。
- M: M表明着真正的执行计算资源。在绑定有效的p后,进入schedule循环;而schedule循环的机制大体是从各类队列、p的本地队列中获取G,切换到G的执行栈上并执行G的函数,调用goexit作清理工做并回到m,如此反复。M并不保留G状态,这是G能够跨M调度的基础。
下面是G、P、M定义的代码片断: //src/runtime/runtime2.go type g struct { stack stack // offset known to runtime/cgo sched gobuf goid int64 gopc uintptr // pc of go statement that created this goroutine startpc uintptr // pc of goroutine function ... ... } type p struct { lock mutex id int32 status uint32 // one of pidle/prunning/... mcache *mcache racectx uintptr // Queue of runnable goroutines. Accessed without lock. runqhead uint32 runqtail uint32 runq [256]guintptr runnext guintptr // Available G's (status == Gdead) gfree *g gfreecnt int32 ... ... } type m struct { g0 *g // goroutine with scheduling stack mstartfn func() curg *g // current running goroutine .... .. }
二、G被抢占调度
和操做系统按时间片调度线程不一样,Go并无时间片的概念。若是某个G没有进行system call调用、没有进行I/O操做、没有阻塞在一个channel操做上,那么m是如何让G停下来并调度下一个runnable G的呢?答案是:G是被抢占调度的。
前面说过,除非极端的无限循环或死循环,不然只要G调用函数,Go runtime就有抢占G的机会。Go程序启动时,runtime会去启动一个名为sysmon的m(通常称为监控线程),该m无需绑定p便可运行,该m在整个Go程序的运行过程当中相当重要:
//$GOROOT/src/runtime/proc.go // The main goroutine. func main() { ... ... systemstack(func() { newm(sysmon, nil) }) .... ... } // Always runs without a P, so write barriers are not allowed. // //go:nowritebarrierrec func sysmon() { // If a heap span goes unused for 5 minutes after a garbage collection, // we hand it back to the operating system. scavengelimit := int64(5 * 60 * 1e9) ... ... if .... { ... ... // retake P's blocked in syscalls // and preempt long running G's if retake(now) != 0 { idle = 0 } else { idle++ } ... ... } }
sysmon每20us~10ms启动一次,按照《Go语言学习笔记》中的总结,sysmon主要完成以下工做:
- 释放闲置超过5分钟的span物理内存;
- 若是超过2分钟没有垃圾回收,强制执行;
- 将长时间未处理的netpoll结果添加到任务队列;
- 向长时间运行的G任务发出抢占调度;
- 收回因syscall长时间阻塞的P;
咱们看到sysmon将“向长时间运行的G任务发出抢占调度”,这个事情由retake实施:
// forcePreemptNS is the time slice given to a G before it is // preempted. const forcePreemptNS = 10 * 1000 * 1000 // 10ms func retake(now int64) uint32 { ... ... // Preempt G if it's running for too long. t := int64(_p_.schedtick) if int64(pd.schedtick) != t { pd.schedtick = uint32(t) pd.schedwhen = now continue } if pd.schedwhen+forcePreemptNS > now { continue } preemptone(_p_) ... ... }
能够看出,若是一个G任务运行10ms,sysmon就会认为其运行时间过久而发出抢占式调度的请求。一旦G的抢占标志位被设为true,那么待这个G下一次调用函数或方法时,runtime即可以将G抢占,并移出运行状态,放入P的local runq中,等待下一次被调度。
三、channel阻塞或network I/O状况下的调度
若是G被阻塞在某个channel操做或network I/O操做上时,G会被放置到某个wait队列中,而M会尝试运行下一个runnable的G;若是此时没有runnable的G供m运行,那么m将解绑P,并进入sleep状态。当I/O available或channel操做完成,在wait队列中的G会被唤醒,标记为runnable,放入到某P的队列中,绑定一个M继续执行。
四、system call阻塞状况下的调度
若是G被阻塞在某个system call操做上,那么不光G会阻塞,执行该G的M也会解绑P(实质是被sysmon抢走了),与G一块儿进入sleep状态。若是此时有idle的M,则P与其绑定继续执行其余G;若是没有idle M,但仍然有其余G要去执行,那么就会建立一个新M。
当阻塞在syscall上的G完成syscall调用后,G会去尝试获取一个可用的P,若是没有可用的P,那么G会被标记为runnable,以前的那个sleep的M将再次进入sleep。
4、调度器状态的查看方法
Go提供了调度器当前状态的查看方法:使用Go运行时环境变量GODEBUG。
$GODEBUG=schedtrace=1000 godoc -http=:6060 SCHED 0ms: gomaxprocs=4 idleprocs=3 threads=3 spinningthreads=0 idlethreads=0 runqueue=0 [0 0 0 0] SCHED 1001ms: gomaxprocs=4 idleprocs=0 threads=9 spinningthreads=0 idlethreads=3 runqueue=2 [8 14 5 2] SCHED 2006ms: gomaxprocs=4 idleprocs=0 threads=25 spinningthreads=0 idlethreads=19 runqueue=12 [0 0 4 0] SCHED 3006ms: gomaxprocs=4 idleprocs=0 threads=26 spinningthreads=0 idlethreads=8 runqueue=2 [0 1 1 0] SCHED 4010ms: gomaxprocs=4 idleprocs=0 threads=26 spinningthreads=0 idlethreads=20 runqueue=12 [6 3 1 0] SCHED 5010ms: gomaxprocs=4 idleprocs=0 threads=26 spinningthreads=1 idlethreads=20 runqueue=17 [0 0 0 0] SCHED 6016ms: gomaxprocs=4 idleprocs=0 threads=26 spinningthreads=0 idlethreads=20 runqueue=1 [3 4 0 10] ... ...
GODEBUG这个Go运行时环境变量非常强大,经过给其传入不一样的key1=value1,key2=value2… 组合,Go的runtime会输出不一样的调试信息,好比在这里咱们给GODEBUG传入了”schedtrace=1000″,其含义就是每1000ms,打印输出一次goroutine scheduler的状态,每次一行。每一行各字段含义以下:
以上面例子中最后一行为例: SCHED 6016ms: gomaxprocs=4 idleprocs=0 threads=26 spinningthreads=0 idlethreads=20 runqueue=1 [3 4 0 10] SCHED:调试信息输出标志字符串,表明本行是goroutine scheduler的输出; 6016ms:即从程序启动到输出这行日志的时间; gomaxprocs: P的数量; idleprocs: 处于idle状态的P的数量;经过gomaxprocs和idleprocs的差值,咱们就可知道执行go代码的P的数量; threads: os threads的数量,包含scheduler使用的m数量,加上runtime自用的相似sysmon这样的thread的数量; spinningthreads: 处于自旋状态的os thread数量; idlethread: 处于idle状态的os thread的数量; runqueue=1: go scheduler全局队列中G的数量; [3 4 0 10]: 分别为4个P的local queue中的G的数量。
咱们还能够输出每一个goroutine、m和p的详细调度信息,但对于Go user来讲,绝大多数时间这是没必要要的:
$ GODEBUG=schedtrace=1000,scheddetail=1 godoc -http=:6060 SCHED 0ms: gomaxprocs=4 idleprocs=3 threads=3 spinningthreads=0 idlethreads=0 runqueue=0 gcwaiting=0 nmidlelocked=0 stopwait=0 sysmonwait=0 P0: status=1 schedtick=0 syscalltick=0 m=0 runqsize=0 gfreecnt=0 P1: status=0 schedtick=0 syscalltick=0 m=-1 runqsize=0 gfreecnt=0 P2: status=0 schedtick=0 syscalltick=0 m=-1 runqsize=0 gfreecnt=0 P3: status=0 schedtick=0 syscalltick=0 m=-1 runqsize=0 gfreecnt=0 M2: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=1 dying=0 helpgc=0 spinning=false blocked=false lockedg=-1 M1: p=-1 curg=17 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 helpgc=0 spinning=false blocked=false lockedg=17 M0: p=0 curg=1 mallocing=0 throwing=0 preemptoff= locks=1 dying=0 helpgc=0 spinning=false blocked=false lockedg=1 G1: status=8() m=0 lockedm=0 G17: status=3() m=1 lockedm=1 SCHED 1002ms: gomaxprocs=4 idleprocs=0 threads=13 spinningthreads=0 idlethreads=7 runqueue=6 gcwaiting=0 nmidlelocked=0 stopwait=0 sysmonwait=0 P0: status=2 schedtick=2293 syscalltick=18928 m=-1 runqsize=12 gfreecnt=2 P1: status=1 schedtick=2356 syscalltick=19060 m=11 runqsize=11 gfreecnt=0 P2: status=2 schedtick=2482 syscalltick=18316 m=-1 runqsize=37 gfreecnt=1 P3: status=2 schedtick=2816 syscalltick=18907 m=-1 runqsize=2 gfreecnt=4 M12: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 helpgc=0 spinning=false blocked=true lockedg=-1 M11: p=1 curg=6160 mallocing=0 throwing=0 preemptoff= locks=2 dying=0 helpgc=0 spinning=false blocked=false lockedg=-1 M10: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 helpgc=0 spinning=false blocked=true lockedg=-1 ... ... SCHED 2002ms: gomaxprocs=4 idleprocs=0 threads=23 spinningthreads=0 idlethreads=5 runqueue=4 gcwaiting=0 nmidlelocked=0 stopwait=0 sysmonwait=0 P0: status=0 schedtick=2972 syscalltick=29458 m=-1 runqsize=0 gfreecnt=6 P1: status=2 schedtick=2964 syscalltick=33464 m=-1 runqsize=0 gfreecnt=39 P2: status=1 schedtick=3415 syscalltick=33283 m=18 runqsize=0 gfreecnt=12 P3: status=2 schedtick=3736 syscalltick=33701 m=-1 runqsize=1 gfreecnt=6 M22: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 helpgc=0 spinning=false blocked=true lockedg=-1 M21: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=0 dying=0 helpgc=0 spinning=false blocked=true lockedg=-1 ... ...
关于go scheduler调试信息输出的详细信息,能够参考Dmitry Vyukov的大做:《Debugging performance issues in Go programs》。这也应该是每一个gopher必读的经典文章。固然更详尽的代码可参考$GOROOT/src/runtime/proc.go中的schedtrace函数。
基础
Go运行时管理调度、垃圾收集和goroutines的运行时环境。在这里,我将只关注调度程序。
运行时调度器经过将它们映射到操做系统线程来运行goroutines。Goroutines是线程的轻量级版本,启动成本很是低。每个goroutine都是由一个名为G的结构体描述的,它包含了跟踪其堆栈和当前状态所必需的字段。因此,G = goroutine。
运行时跟踪每一个G,并将它们映射到逻辑处理器上,命名为P。P能够被看做是一个抽象的资源或上下文,须要被获取,所以OS线程(称为M或机器)能够执行G。你能够经过调用 runtime.GOMAXPROCS(numLogicalProcessors) 来控制运行时的逻辑处理器,若是你打算调整这个参数(或许不该该),设置一次并忘记它,由于它须要“中止一切”GC暂停。
从本质上讲,操做系统运行线程,执行你的代码。Go的诀窍是,编译器在不一样的地方插入调用到Go运行时,例如经过通道发送一个值,对运行时包进行调用),这样就能够通知调度程序并采起行动。
Ms,Ps&Gs之间的互动
Ms、Ps和Gs之间的交互有点复杂。看一下这个工做流程图:
在这里咱们能够看到,对于G来讲有两种类型的队列:在“schedt”结构中有一个全局队列(不多使用),而且每一个P维护一个可运行的G队列。
为了执行一个goroutine,M须要保存上下文P.机器,而后弹出它的goroutines,执行代码。
当你安排一个新的goroutine(作一个go func()调用)时,它被放置到P的队列中。这里有一个有趣的偷工调度算法,当M完成了某个G的执行,而后它试图从队列中取出另外一个G,它是空的,而后它随机地选择另外一个P并试图从它中偷取一半的可运行的G!
当你的goroutine作一个阻塞的系统调用时,会发生一些有趣的事情。阻塞系统调用将被拦截,若是要运行Gs,运行时将从P中分离出线程并建立一个新的OS线程(若是空闲线程不存在的话)来服务该处理器。
当一个系统调用恢复时,goroutine被放回一个本地运行队列,线程会自动放置(意味着线程不会运行),并将本身插入到空闲线程列表中。
若是goroutine进行网络调用,运行时也会执行相似的操做。这个调用将被拦截,可是由于Go有一个集成的网络轮询器,它有本身的线程,它将被分配给它。
若是当前的goroutine被阻塞,那么运行时将运行一个不一样的goroutine:
-
阻塞系统调用(例如打开一个文件),
-
网络输入,
-
通道操做,
-
同步包中的原语。
调度程序跟踪
Go容许跟踪运行时调度程序。这是经过GODEBUG环境变量完成的:
$ GODEBUG=scheddetail=1,schedtrace=1000 ./program
下面是它给出的输出示例:
SCHED 0ms: gomaxprocs=8 idleprocs=7 threads=2 spinningthreads=0 idlethreads=0 runqueue=0 gcwaiting=0 nmidlelocked=0 stopwait=0 sysmonwait=0 P0: status=1 schedtick=0 syscalltick=0 m=0 runqsize=0 gfreecnt=0 P1: status=0 schedtick=0 syscalltick=0 m=-1 runqsize=0 gfreecnt=0 P2: status=0 schedtick=0 syscalltick=0 m=-1 runqsize=0 gfreecnt=0 P3: status=0 schedtick=0 syscalltick=0 m=-1 runqsize=0 gfreecnt=0 P4: status=0 schedtick=0 syscalltick=0 m=-1 runqsize=0 gfreecnt=0 P5: status=0 schedtick=0 syscalltick=0 m=-1 runqsize=0 gfreecnt=0 P6: status=0 schedtick=0 syscalltick=0 m=-1 runqsize=0 gfreecnt=0 P7: status=0 schedtick=0 syscalltick=0 m=-1 runqsize=0 gfreecnt=0 M1: p=-1 curg=-1 mallocing=0 throwing=0 preemptoff= locks=1 dying=0 helpgc=0 spinning=false blocked=false lockedg=-1 M0: p=0 curg=1 mallocing=0 throwing=0 preemptoff= locks=1 dying=0 helpgc=0 spinning=false blocked=false lockedg=1 G1: status=8() m=0 lockedm=0
注意,它使用了与G、M和P以及它们的状态相同的概念,好比P的队列大小。一般,你不须要那么多的细节,因此你可使用:
$ GODEBUG=schedtrace=1000 ./program
此外,还有一个名为go tool trace的高级工具,它有一个UI,容许咱们探索,程序运行时正在作什么。
MPG画图示意
调度模型简介
groutine能拥有强大的并发实现是经过GPM调度模型实现,下面就来解释下goroutine的调度模型。
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的队列以及调度器的一些状态信息等。
调度实现
从上图中看,有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线程都能充分的使用,以下图:
参考地址:
http://morsmachine.dk/go-scheduler