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 的本地队列已经满了就会保存在全局的队列中;
3. G 只能运行在 M 中,一个 M 必须持有一个 P,M 与 P 是 1:1 的关系。M 会从 P 的本地队列弹出一个可执行状态的 G 来执行,如果 P 的本地队列为空,就会想其他的 MP 组合偷取一个可执行的 G 来执行;
4. 一个 M 调度 G 执行的过程是一个循环机制; - 当 M 执行某一个 G 时候如果发生了 syscall 或则其余阻塞操作,M 会阻塞,如果当前有一些 G 在执行,runtime 会把这个线程 M 从 P 中摘除 (detach),然后再创建一个新的操作系统的线程 (如果有空闲的线程可用就复用空闲线程) 来服务于这个 P;
- 当 M 系统调用结束时候,这个 G 会尝试获取一个空闲的 P 执行,并放入到这个 P 的本地队列。如果获取不到 P,那么这个线程 M 变成休眠状态, 加入到空闲线程中,然后这个 G 会被放入全局队列中。
调度器的生命周期
Have Fun