GMP的发展: go 1.1版本之前时候过使用的是GM模型+全局队列的模式。
GM模型+全局队列的模式
M:1 = 内核线程:协程
![](https://i-blog.csdnimg.cn/blog_migrate/b820551ee85de2416dd8e1789a390201.png)
新建一个协程G的时候会放入全局队列中,每次执行一个协程G的时候,内核线程M会从全局队列中获取一个协程G执行,因为内核线程M存在多个所以存在并发问题,因此每次从队列中取协程G的时候都要加锁,所以当高并发的时候就会存在性能问题。
解决办法:给内核线程分配一个协程队列
GMP模型 + 全局队列
M:N:=内核线程 :协程
p的个数对应的是设置的内核个数
GMP模型 :
G -> goroutine(协程)
P -> Processor 调度器(如果线程想运行goroutine,必须先获取P,P中还包含了可运行的G队列)(注意不是cpu)
M -> thread 内核线程(每个M都代表了1个内核线程,OS调度器负责把内核线程分配到CPU的核上执行。)
模型简介
![](https://i-blog.csdnimg.cn/blog_migrate/fc4f830093c710564abd087d2116ab3e.png)
调度器的设计策略
并行(多核多线程来实现并行)、抢占(限定G与M的绑定时间,最多10ms,超过时间解绑,避免其他协程饥饿)、复用线程(偷取 + 分离)、减少全局竞争
有多个P M来执行多个G;p本地队列最多256g
![](https://i-blog.csdnimg.cn/blog_migrate/a28df8934b7061ce8220f5357a03c154.png)
新建一个协程G会优先放到本队队列中,如果本队队列P满了,则会把G放入到全局队列中,本地队列为空的时候,就会从全局队列中获取,如果全局队列为空的话,就会从其他队里中拿协程到自己的本地队列中。如果中途携程阻塞了,本地队列会在其他的内核线程上运行。
注意:M的数量和P的数量没有关系。如果当前的M阻塞,P的goroutine会运行在其他的M上,或者新建一个M。所以可能出现有很多个M,只有1个P的情况。
“go func()”经历了什么过程
![](https://i-blog.csdnimg.cn/blog_migrate/cdcc845b45f90ebac04ee9bbf18e5963.png)
调度器的生命周期
![](https://i-blog.csdnimg.cn/blog_migrate/fe2ef406db0f37afc119319907e43380.png)
GO生命周期:创建、保存、被获取、调度执行、阻塞、销毁
CPU感知不到协程,GO调度器把协程调度到内核线程上去,然后操作系统调度器将内核线程放到cpu上执行;m是内核线程的封装。go调度器工作就是将G分配到M
M的几种状态:休眠(未绑定P)、运行、自旋(绑定了p 只是没有可执行G)。
运行+自旋<= gomaxproc数量
需要注意的点:
M和P不是绝对的1:1 ,有G阻塞的时候,M也会阻塞,会有新的M来继续执行P队中的G
p队列是先进先出
work stealing时,从偷取的P那里,取尾部的一半放到自己的P中
从全局队列中取时,取n个,len(GQ)为全局队列G的个数,公式: n = min(len(GQ)/GOMAXPROCS + 1, len(GQ/2))
本地队列满了,又创建新的G,此时把本地队列的前一半与新创建的G,打乱后,一起放到全局队列中
自旋线程的最大限制不能超过GOMAXPROCS,多出的线程休眠(⻓时间休眠等待GC回收销毁)
原文链接:https://blog.csdn.net/qq_42956653/article/details/121234816
扩展
如何控制同一时间的并发执行数,不能让程序崩溃?
(1)使用sync.WaitGroup控制协程数量。不是动态控制法,只是控制并发了
(2)使用sync.Mutex控制协程数量 ,这个待验证
(3) 使用带缓冲的通道限制并发数 。创建chan的时候手动设置缓存: c:=make(chan int, 2), 在协程创建前写入chan在协程结束前读出chan,达到限制并发数的需求
![](https://i-blog.csdnimg.cn/blog_migrate/3b1e2d39e7148bbac3eb2257a033e123.jpeg)
有关M和P的个数问题
P的数量:
由启动时环境变量 $GOMAXPROCS 或者是由 runtime 的方法 GOMAXPROCS() 决定。这意味着在程序执行的任意时刻都只有 $GOMAXPROCS 个 goroutine 在同时运行。
M的数量:
go 语言本身的限制:go 程序启动时,会设置 M 的最大数量,默认 10000. 但是内核很难支持这么多的线程数,所以这个限制可以忽略。
runtime/debug 中的 SetMaxThreads 函数,设置 M 的最大数量
一个 M 阻塞了,会创建新的 M。
M 与 P 的数量没有绝对关系,一个 M 阻塞,P 就会去创建或者切换另一个 M,所以,即使 P 的默认数量是 1,也有可能会创建很多个 M 出来。
P 和 M 何时会被创建?
在确定了 P 的最大数量 n 后,运行时系统会根据这个数量创建 n 个 P。
没有足够的 M 来关联 P 并运行其中的可运行的 G。比如所有的 M 此时都阻塞住了,而 P 中还有很多就绪任务,就会去寻找空闲的 M,而没有空闲的,就会去创建新的 M。
调度器的设计策略?
复用线程:避免频繁的创建、销毁线程,而是对线程的复用。
work stealing 机制
当本线程无可运行的 G 时,尝试从其他线程绑定的 P 偷取 G,而不是销毁线程。
hand off 机制
当本线程因为 G 进行系统调用阻塞时,线程释放绑定的 P,把 P 转移给其他空闲的线程执行。
利用并行:GOMAXPROCS 设置 P 的数量,最多有 GOMAXPROCS 个线程分布在多个 CPU 上同时运行。GOMAXPROCS 也限制了并发的程度,比如 GOMAXPROCS = 核数/2,则最多利用了一半的 CPU 核进行并行。
抢占:在 coroutine 中要等待一个协程主动让出 CPU 才执行下一个协程,在 Go 中,一个 goroutine 最多占用 CPU 10ms,防止其他 goroutine 被饿死,这就是 goroutine 不同于 coroutine 的一个地方。
全局 G 队列:,当 M 执行 work stealing 从其他 P 偷不到 G 时,它可以从全局 G 队列获取 G。
调度器的生命周期?
![](https://i-blog.csdnimg.cn/blog_migrate/daa463e9215d5a4671f2a29f67ffa823.png)
M0
M0 是启动程序后的编号为 0 的主线程,这个 M 对应的实例会在全局变量 runtime.m0 中,不需要在 heap 上分配,M0 负责执行初始化操作和启动第一个 G, 在之后 M0 就和其他的 M 一样了。
G0
G0 是每次启动一个 M 都会第一个创建的 gourtine,G0 仅用于负责调度的 G,G0 不指向任何可执行的函数,每个 M 都会有一个自己的 G0。在调度或系统调用时会使用 G0 的栈空间,全局变量的 G0 是 M0 的 G0。
可视化 GMP 编程?
方式 1:go tool trace 方式 2:Debug trace
更多详情点击查看点击这里