每个OS线程都有一个固定大小的内存块(通常会是2MB)来作栈,这个栈会用来存储当前正在被调用或挂起(指在调用其它函数时)的函数的内部变量。这个固定大小的栈同时很大又很小。由于2MB的栈对于一个小小的goroutine来讲是很大的内存浪费,而对于一些复杂的任务(如深度嵌套的递归)来讲又显得过小。所以,Go语言作了它本身的『线程』。golang
在Go语言中,每个goroutine是一个独立的执行单元,相较于每一个OS线程固定分配2M内存的模式,goroutine的栈采起了动态扩容方式, 初始时仅为2KB,随着任务执行按需增加,最大可达1GB(64位机器最大是1G,32位机器最大是256M),且彻底由golang本身的调度器 Go Scheduler 来调度。此外,GC还会周期性地将再也不使用的内存回收,收缩栈空间。 所以,Go程序能够同时并发成千上万个goroutine是得益于它强劲的调度器和高效的内存模型。Go的创造者大概对goroutine的定位就是屠龙刀,由于他们不只让goroutine做为golang并发编程的最核心组件(开发者的程序都是基于goroutine运行的)并且golang中的许多标准库的实现也处处能见到goroutine的身影,好比net/http这个包,甚至语言自己的组件runtime运行时和GC垃圾回收器都是运行在goroutine上的,做者对goroutine的厚望可见一斑。web
任何用户线程最终确定都是要交由OS线程来执行的,goroutine(称为G)也不例外,可是G并不直接绑定OS线程运行,而是由Goroutine Scheduler中的 P - Logical Processor (逻辑处理器)来做为二者的『中介』,P能够看做是一个抽象的资源或者一个上下文,一个P绑定一个OS线程,在golang的实现里把OS线程抽象成一个数据结构:M,G其实是由M经过P来进行调度运行的,可是在G的层面来看,P提供了G运行所需的一切资源和环境,所以在G看来P就是运行它的 “CPU”,由 G、P、M 这三种由Go抽象出来的实现,最终造成了Go调度器的基本结构:算法
关于P,咱们须要再絮叨几句,在Go 1.0发布的时候,它的调度器其实G-M模型,也就是没有P的,调度过程全由G和M完成,这个模型暴露出一些问题:编程
这些问题实在太扎眼了,致使Go1.0虽然号称原生支持并发,却在并发性能上一直饱受诟病,而后,Go语言委员会中一个核心开发大佬看不下了,亲自下场从新设计和实现了Go调度器(在原有的G-M模型中引入了P)而且实现了一个叫作 work-stealing 的调度算法:缓存
该算法避免了在goroutine调度时使用全局锁。网络
至此,Go调度器的基本模型确立:数据结构
Go调度器工做时会维护两种用来保存G的任务队列:一种是一个Global任务队列,一种是每一个P维护的Local任务队列。并发
当经过go
关键字建立一个新的goroutine的时候,它会优先被放入P的本地队列。为了运行goroutine,M须要持有(绑定)一个P,接着M会启动一个OS线程,循环从P的本地队列里取出一个goroutine并执行。固然还有上文说起的 work-stealing
调度算法:当M执行完了当前P的Local队列里的全部G后,P也不会就这么在那躺尸啥都不干,它会先尝试从Global队列寻找G来执行,若是Global队列为空,它会随机挑选另一个P,从它的队列里中拿走一半的G到本身的队列中执行。函数
若是一切正常,调度器会以上述的那种方式顺畅地运行,但这个世界没这么美好,总有意外发生,如下分析goroutine在两种例外状况下的行为。性能
Go runtime会在下面的goroutine被阻塞的状况下运行另一个goroutine:
这四种场景又可归类为两种类型:
当goroutine由于channel操做或者network I/O而阻塞时(实际上golang已经用netpoller实现了goroutine网络I/O阻塞不会致使M被阻塞,仅阻塞G,这里仅仅是举个栗子),对应的G会被放置到某个wait队列(如channel的waitq),该G的状态由_Gruning
变为_Gwaitting
,而M会跳过该G尝试获取并执行下一个G,若是此时没有runnable的G供M运行,那么M将解绑P,并进入sleep状态;当阻塞的G被另外一端的G2唤醒时(好比channel的可读/写通知),G被标记为runnable,尝试加入G2所在P的runnext,而后再是P的Local队列和Global队列。
当G被阻塞在某个系统调用上时,此时G会阻塞在_Gsyscall
状态,M也处于 block on syscall 状态,此时的M可被抢占调度:执行该G的M会与P解绑,而P则尝试与其它idle的M绑定,继续执行其它G。若是没有其它idle的M,但P的Local队列中仍然有G须要执行,则建立一个新的M;当系统调用完成后,G会从新尝试获取一个idle的P进入它的Local队列恢复执行,若是没有idle的P,G会被标记为runnable加入到Global队列。
以上就是从宏观的角度对Goroutine和它的调度器进行的一些概要性的介绍,固然,Go的调度中更复杂的抢占式调度、阻塞调度的更多细节,你们能够自行去找相关资料深刻理解,本文只讲到Go调度器的基本调度过程,为后面本身实现一个Goroutine Pool提供理论基础,这里便再也不继续深刻上述说的那几个调度了,事实上若是要彻底讲清楚Go调度器,一篇文章的篇幅也实在是捉襟见肘,因此想了解更多细节的同窗能够去看看Go调度器 G-P-M 模型的设计者 Dmitry Vyukov 写的该模型的设计文档《 Go Preemptive Scheduler Design》以及直接去看源码,G-P-M模型的定义放在src/runtime/runtime2.go
里面,而调度过程则放在了src/runtime/proc.go
里。
REFERENCE: