Golang的goroutine模型概述和调度
G-P-M 模型概述 每一个OS线程都有一个固定大小的内存块(一般会是2MB)来做栈,这个栈会用来存储当前正在被调用或挂起(指在调用其它函数时)的函数的内部变量。 这个固定大小的栈同时很大又很小。因为2MB的栈对于一个小小的Goroutine来说是很大的内存浪费,而对于一些复杂的任务(如深度嵌套的递归)来说又显得太小。因此,Go语言做了它自己的『线程』。 在Go语言中,每一个Goroutine是一个独立的执行单元,相较于每个OS线程固定分配2M内存的模式,Goroutine的栈采取了动态扩容方式, 初始时仅为2KB,随着任务执行按需增长,最大可达1GB(64位机器最大是1G,32位机器最大是256M),且完全由Golang自己的调度器 Go Scheduler 来调度。 此外,GC还会周期性地将不再使用的内存回收,收缩栈空间。 因此,Go程序可以同时并发成千上万个Goroutine是得益于它强劲的调度器和高效的内存模型。 任何用户线程最终肯定都是要交由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调度器的基本结构: G: Goroutine G有以下状态 非GC状态 idle:_Gidle for idle,意思是这个goroutine刚被创建出来,还未被进行初始化。 runnable: _Grunnable for runnable意思是这个goroutine已经在运行队列,在这种情况下,goroutine还未执行用户代码,M的执行栈还不是goroutine自己的 running: _Grunning for running,意思是goroutine可能正在执行用户代码,M的执行栈已经由该goroutine所拥有,此时对象g不在运行队列中。这个状态值要待分配给M和P之后,交由M和P来设定 syscall, waiting, dead, copystack 对应的GC状态 scan, scanrunnable, scan running, scansyscall, scanwaiting _Gscan系列,用于标记正在被GC扫描的状态,这些状态是由_Gscan=0x1000再加上_GRunnable, _Grunning, _Gsyscall和_Gwaiting的枚举值所产生的,这么做的好处是直接通过简单的运算即可知道被Scan之前的状态。当被标记为这系列的状态时,这些goroutine都不会执行用户代码,并且它们的执行栈都是被做该GC的goroutine所拥有。不过_Gscanrunning状态有点特别,这个标记是为了阻止正在运行的goroutine切换成其它状态,并告诉这个G自己扫描自己的堆栈。正是这种巧妙的方式,使得Go语言的GC十分高效。 每个Goroutine对应一个G结构体,G 存储 Goroutine的运行堆栈、状态以及任务函数,可重用。 G并非执行体,每个G需要绑定到P才能被调度执行。 P: Processor 表示逻辑处理器, 对G来说,P相当于CPU核,G只有绑定到P(在P的local run中)才能被调度。对M来说,P提供了相关的执行环境(Context),如内存分配状态(mcache),任务队列(G)等,P的数量决定了系统内最大可并行的G的数量(前提:物理CPU核数 >= P的数量),P的数量由用户设置的GoMAXPROCS决定,但是不论GoMAXPROCS设置为多大,P的数量最大为256。 golang runtime是有个sysmon的协程,他会轮询的检测所有的P上下文队列,**只要 G-M 的线程长时间在阻塞状态,那么就重新创建一个线程去从runtime P队列里获取任务。先前的阻塞的线程会被游离出去了,当他完成阻塞操作后会触发相关的callback回调,并加入回线程组里。**简单说,如果你没有特意配置runtime.SetMaxThreads,那么在没有可复用的线程的情况下,会一直创建新线程。 M: Machine OS线程抽象,代表着真正执行计算的资源。 在绑定有效的P后,进入schedule循环;而schedule循环的机制大致是从Global队列、P的Local队列以及wait队列中获取G,切换到G的执行栈上并执行G的函数,调用Goexit做清理工作并回到M,如此反复。 M并不保留G状态,这是G可以跨M调度的基础,M的数量是不定的,由Go Runtime调整,为了防止创建过多OS线程导致系统调度不过来,目前默认最大限制为10000个。 在绝大多数时候,其实P的数量和M的数量是相等。 每创建一个p, 就会创建一个对应的M只有少数情况下,M的数量会大于P work-stealing 的调度算法 每个P维护一个G的本地队列; 当一个G被创建出来,或者变为可执行状态时,就把他放到P的可执行队列中; 当一个G在M里执行结束后,P会从队列中把该G取出;如果此时P的队列为空,即没有其他G可以执行, M就随机选择另外一个P,从其可执行的G队列中取走一半。 ...