GMP 线程调度模型
GMP 线程调度模型
GMP线程调度模型是Go协程调度的CSP并发模型实现,是对两级线程模型进行了一定程度的改进,使它能够更加灵活地进行线程之间的调度。
Golang内部有三个对象,goroutine,machine,processor。
研究这块最好结合源码。
GMP 调度模型
G=Goroutine协程,P=Processor处理器,M=Thread线程
- 全局队列(
Global Queue):存放等待运行的G。 P的本地队列:同全局队列类似,存放的也是等待运行的G,存的数量有限,不超过256个。新建G’时,G’优先加入到P的本地队列,如果队列满了,则会把本地队列中一半的G移动到全局队列。P:所有的P都在程序启动时创建,并保存在数组中,最多有GOMAXPROCS(可配置) 个。M:线程想运行任务就得获取P,从P的本地队列获取G,P队列为空时,M也会尝试从全局队列拿一批G放到P的本地队列,或从其他P的本地队列偷一半放到自己P的本地队列。M运行G,G执行之后,M会从P获取下一个G,不断重复下去。
说明:
G,Goroutine,协程,轻量级的用户线程,是对Go语言中代码片段的封装;M,Machine,物理线程(工作线程),对内核级线程的封装,一个Machine对应一个内核线程,当前操作系统分配到当前Go程序的线程数,由于Go语言本身的限制,Go程序启动时会设置M的最大数量默认为10000,但是内核很难支持这么多的线程数,所以这个限制可以忽略。可以通过runtime/debug中的SetMaxThreads函数来设置M的最大数量;P,Processor,处理器(上下文),G和M的调度对象,用来调度G和M之间的关联关系,程序启动时创建,其数量可通过GOMAXPROCS()来设置,默认为CPU核心数,这意味着在程序执行的任意时刻都只有GOMAXPROCS个goroutine在同时运行。
Goroutine调度器和OS调度器是通过M结合起来的,每个M都会与1个内核线程绑定,OS调度器负责把内核线程分配到CPU的核上执行。在运行时一个M同时只能绑定1个P,M和P是一对一绑定的,M和P的组合共同构成了G的有效运行环境。但M和P会适时的组合和断开,以保证待执行G队列能够得到及时执行。而P和G的关系是一对多的,多个可执行G将会顺序排成一个队列挂在某个P上面。在运行过程中,M和内核线程之间的对应关系不会变,在M的生命周期内,它只会和一个内核线程绑定,而M和P以及P和G之间的关系都是动态可变的。
M与P的数量没有绝对关系,一个M阻塞,P就会去创建或者切换另一个M,所以,即使P的默认数量是1,也有可能会创建很多个M出来。但由于P的存在,G和M可以呈现出多对多的关系。当一个正在与某个M对接并运行着的G,需要因某个事件(比如等待I/O或锁的解除)而暂停运行的时候,调度器总会及时地发现,并把这个G与那个M分离开,以释放计算资源供那些等待运行的G使用。
P和M何时会被创建
P何时创建:在确定了P的最大数量n后,运行时系统会根据这个数量创建n个P。M何时创建:没有足够的M来关联P并运行其中的可运行的G。比如所有的M此时都阻塞住了,而P中还有很多就绪任务,就会去寻找空闲的M,而没有空闲的,就会去创建新的M。
go func() 调度流程
- 我们通过
go func ()来创建一个goroutine; - 有两个存储
G的队列,一个是局部调度器P的本地队列、一个是全局G队列。新创建的G会先保存在P的本地队列中,如果P的本地队列已经满了就会保存在全局的队列中; G只能运行在M中,一个M必须持有一个P,M与P是1:1的关系。M会从P的本地队列弹出一个可执行状态的G来执行,如果P的本地队列为空,就会想其他的MP组合偷取一个可执行的G来执行;- 一个
M调度G执行的过程是一个循环机制; - 当
M执行某一个G时候如果发生了syscall或则其余阻塞操作,M会阻塞,如果当前有一些G在执行,runtime会把这个线程M从P中摘除 (detach),然后再创建一个新的操作系统的线程 (如果有空闲的线程可用就复用空闲线程) 来服务于这个P; - 当
M系统调用结束时候,这个G会尝试获取一个空闲的P执行,并放入到这个P的本地队列。如果获取不到P,那么这个线程M变成休眠状态, 加入到空闲线程中,然后这个G会被放入全局队列中。
调度器的生命周期
本文由作者按照 CC BY 4.0 进行授权


