Golang GMP调度模型

1. 背景

单进程时代

每个程序就是一个进程,一个程序运行完,才能进行下一个进程,串行化执行,当一个进程被IO阻塞时,CPU只能等待IO事件就绪继续执行,而不能切换到其他进程执行,造成CPU资源的浪费。

多进程时代

后来操作系统具备了最早的并发能力:多进程并发,当一个进程阻塞的时候,切换到其他等待的进程执行,这样就减少了CPU资源的浪费。
调度cpu的算法可以保证在运行的进程都可以被分配到CPU的运行时间片。宏观上看,似乎多个进程同时运行。但新的问题就又出现了,进程拥有太多的资源,进程的创建、切换、销毁,都会占用CPU时间,这些过程也会浪费CPU资源,尤其在进程过多时,CPU的利用率就会更低。

多线程时代

为了进一步提交CPU利用率,就有了多线程
每一个程序都会创建一个进程,并且每一个进程都会有一个初始线程。线程可以创建更多线程。不同的线程独立运行,调度的最小单元是线程而不是进程。线程可以并发(同核),或并行(不同核)的执行,从而充分利用CPU。但仍然存在高内存占用和调度时高CPU消耗

协程时代

为了解决线程的问题,就有了协程,能有效解决线程上下文切换的成本问题
线程状态分为用户态和内核态,我们能改变的是用户态,内核态由操作系统控制。
工程师们发现,其实一个线程分为“内核态“线程和”用户态“线程。一个“用户态线程”必然要绑定一个“内核态线程”,但是CPU并不知道有“用户态线程”的存在,它只知道运行的是一个“内核态线程”(Linux的PCB进程控制块)。这样,再细化分类一下,内核线程依然叫“线程(thread)”,用户线程叫“协程(co-routine)”。

2. 概念说明

G(goroutine):goroutine实体,用户态线程,Goroutine的sched保存着其上下文信息
M(machine):对内核线程的封装,用来运行G,数量等于CPU核数
P(processor):调度器,相当于一个调度器,它包含了goroutine运行的资源,M必须和一个P关联才能运行G。其数量可通过GOMAXPROCS()来设置,默认为核心数,其包含一个本地队列用于存储等待执行的G列表,数量不超过256个
当创建新的G时,先检查本地队列是否满,如果有位置,就加入到P的本地队列,否则就加入到全局队列

3. GM调度模型

M从加锁的Goroutine全局队列中获取G执行,如果G在运行过程中创建了新的G,那么新的G也会被放入全局队列中。

缺点,一调度,获取G都需要获取队列锁,形成了激烈的竞争。二M转移G没有把资源最大化利用。比如当M1在执行G1时,M1创建了G2,为了继续执行G1,需要把G2交给M2执行,因为G1和G2是相关的,而寄存器中会保存G1的信息,因此G2最好放在M1上执行,而不是其他的M。

4. GMP调度模型

相比GM模型,GMP模型在GM之间加了一层P,来解决GM模型的缺点,减少了全局队列的竞争激烈程度,降低了G可能的转移开销。

调度步骤

4.1 每个P维护一个本地队列;
4.2 当一个G被创建后,首先尝试放入P的本地队列中,如果队列已满,放入全局队列;
4.3 当M执行完一个G后,会在当前P的本地队列中取出新的G,队列为空时则去全局队列中加锁获取;如果全局队列也为空,则去其他的P的本地队列中偷出一半的G,放入自己的P本地队列
4.4 M执行G,如果G发生系统调用或阻塞,则创建一个M或唤醒一个休眠的M来接管这个P本地队列
4.5 G执行完成后,销毁G,返回结果

在这里插入图片描述

5. 主要函数

核心文件
src/runtime/proc.go
src/runtime/runtime1.go
src/runtime/runtime2.go

func mstart
mstart is the entry-point for new Ms.

func schedule
One round of scheduler: find a runnable goroutine and execute it.
Never returns.

func findrunnable
M Finds a runnable goroutine to execute.
Tries to steal from other P’s, get g from local or global queue, poll network.

func execute
Schedules gp to run on the current M

func runqsteal
Steal half of elements from local runnable queue of p2,and put onto local runnable queue of p. Returns one of the stolen elements (or nil if failed)
注意:如果能找到本地队列非空的p2,则偷取p2一半的G,然后写入p自己的本地队列

func newproc
Create a new g running fn
Put it on the queue of g’s waiting to run
注意:它本质上调用的是func newproc1

func newproc1
Create a new g in state _Grunnable, starting at fn. callerpc is the address of the go statement that created this. The caller is responsible for adding the new g to the scheduler

func gfput
Put on gfree list.
If local list is too long, transfer a batch to the global list.

func gfget
Get from gfree list.
If local list is empty, grab a batch from global list.

func stealwork
stealWork attempts to steal a runnable goroutine or timer from any P.

6. 几个问题

1 mp的数量关系,m,p数量不是绝对的1:1关系,当G执行被阻塞时,就可能创建新的M,P的数量由$GOMAXPROCS限制,M的最大值是10000
2 设计策略:复用线程,并行,抢占
3 stealwork 会偷取一半,目的是为了保持平衡
4

参考文档
1 github.com/golang/go
2 http://www.icodebang.com/article/290614
3 《go语言设计与实现》

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值