目录
1.Goroutine 定义
2.GMP 指的是什么
3.GMP模型的简介
全局队列(Global Queue)
P的本地队列
P列表
M列表
4.有关P和M的个数问题
P的数量问题
M的数量问题
P和M何时会被创建
5.调度器P的设计策略
复⽤线程
work stealing机制
hand off机制
利⽤并⾏
抢占
全局G队列
6.“go func()” 经历了什么过程
7.调度器的生命周期
M0
G0
8.Sysmon有什么作用
9.可视化GMP编程
1.Goroutine 定义
定义: Goroutine 是 Go 语言运行时系统(Runtime)管理的用户级线程(User-Level Thread),是一种可以在单个进程中并发执行的执行单元。每个 Goroutine 代表了一个独立的函数调用,可以在程序中并行地执行任务,而无需操作系统层面的线程(OS Thread)支持。
关键特性:
-
轻量级:相较于操作系统原生线程,Goroutine 的创建、销毁和上下文切换(Context Switching)成本极低,使得开发者可以轻松地在程序中创建大量并发任务,实现高并发处理。
-
协程:Goroutines 之间通过协作而非抢占式调度进行切换,这意味着一个 Goroutine 只有在主动放弃 CPU 时间片(如调用系统调用、阻塞在 I/O 操作或显式调用
sync
包中的同步原语)时,运行时才会调度其他等待的 Goroutine 执行。这种非抢占式的调度有助于减少线程上下文切换的开销。 -
Go 语言原生支持:在 Go 语言中,启动一个新的 Goroutine 非常简单,只需在函数调用前加上关键字
go
,如go someFunction()
。编译器会负责将此函数封装为一个可以并发执行的 Goroutine。 -
调度器:Go 语言的运行时系统内置了一个高效的调度器(Scheduler),负责管理和调度所有 Goroutines 的执行。调度器可以根据系统资源(如 CPU 核心数)和 Goroutines 的运行状态动态调整其调度策略,确保并发任务高效、公平地执行。
-
通信代替共享内存:Go 语言鼓励通过 channels(通道)进行 Goroutines 之间的通信和同步,遵循“不要通过共享内存来通信,而应通过通信来共享内存”的原则,避免了传统并发编程中常见的数据竞争和竞态条件问题。
-
栈动态增长:每个 Goroutine 初始时分配一个小栈(通常为几 KB),随着函数调用深度的增加,栈空间不足时会自动增长(可达几 MB),避免了预估栈大小的难题,同时也减少了内存浪费。
综上所述,Goroutine 是 Go 语言中实现并发编程的关键特性,它是一种轻量级、协程化的执行单元,由 Go 语言运行时系统原生支持并高效调度。通过 Goroutines,开发者可以方便、高效地编写并发程序,利用多核处理器能力,同时借助 channels 和其他同步原语确保数据安全和正确性。
2.GMP 指的是什么
- G( Goroutine): 我们所说的协程,为用户级的轻量级线程,每个Goroutine对象中的sched保存着其上下文信息。
- M( Machine): 对内核级线程的封装,数量对应真实的 CPU 数(真正干活的对象)。
- P( Processor): 即为 G 和 M 的调度对象,用来调度 G 和 M 之间的关联关系,其数量可通过 GOMAXPROCS()来设置,默认为核心数。
3.GMP模型的简介
在Go中,线程是运行goroutine的实体,调度器的功能是把可运行的goroutine分配到工作线程上
全局队列(Global Queue)
存放等待运行的G。
P的本地队列
- 同全局队列类似,存放的也是等待运行的G
- 存的数量有限,不超过256个。
- 新建G'时,G'优先加入到P的本地队列,如果队列满了,则会把本地队列中一半的G移动到全局队列。
P列表
所有的P都在程序启动时创建,并保存在数组中,最多有
GOMAXPROCS
(可配置)个。
M列表
- 当前操作系统分配到当前Go程序的内核线程数
- 线程想运行任务就得获取P,从P的本地队列获取G,P队列为空时,M也会尝试从全局队列拿一批G放到P的本地队列,或从其他P的本地队列偷一半放到自己P的本地队列。
- M运行G,G执行之后,M会从P获取下一个G,不断重复下去。
Goroutine调度器和OS调度器是通过M结合起来的,每个M都代表了1个内核线程,OS调度器负责把内核线程分配到CPU的核上执行。
4.有关P和M的个数问题
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何时创建:在确定了P的最大数量n后,运行时系统会根据这个数量创建n个P。
- M何时创建:没有足够的M来关联P并运行其中的可运行的G。比如所有的M此时都阻塞住了,而P中还有很多就绪任务,就会去寻找空闲的M,而没有空闲的,就会去创建新的M。
5.调度器P的设计策略
复⽤线程
避免频繁的创建、销毁线程,⽽是对线程的复⽤。
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队列
在新的调度器中依然有全局G队列,当P的本地队列为空时,优先从全局队列获取,如果全局队列为空时则通过work stealing机制从其他P的本地队列偷取G。
6.“go func()” 经历了什么过程
1、我们通过 go func()来创建一个goroutine;
2、有两个存储G的队列,一个是局部调度器P的本地队列、一个是全局G队列。新创建的G会先保存在P的本地队列中,如果P的本地队列已经满了就会保存在全局的队列中;
3、G只能运行在M中,一个M必须持有一个P,M与P是1:1的关系。M会从P的本地队列弹出一个可执行状态的G来执行,如果P的本地队列为空,就会想其他的MP组合偷取一个可执行的G来执行;
4、一个M调度G执行的过程是一个循环机制;
5、当M执行某一个G时候如果发生了syscall或则其余阻塞操作,M会阻塞,如果当前有一些G在执行,runtime会把这个线程M从P中摘除(detach),然后再创建一个新的操作系统的线程(如果有空闲的线程可用就复用空闲线程)来服务于这个P;
6、当M系统调用结束时候,这个G会尝试获取一个空闲的P执行,并放入到这个P的本地队列。如果获取不到P,那么这个线程M变成休眠状态, 加入到空闲线程中,然后这个G会被放入全局队列中。
7.调度器的生命周期
M0
M0
是启动程序后的编号为0的主线程,这个M对应的实例会在全局变量runtime.m0中,不需要在heap上分配,M0负责执行初始化操作和启动第一个G, 在之后M0就和其他的M一样了。
G0
是每次启动一个M都会第一个创建的gourtine,G0仅用于负责调度的G,G0不指向任何可执行的函数, 每个M都会有一个自己的G0。在调度或系统调用时会使用G0的栈空间, 全局变量的G0是M0的G0。
我们来跟踪一段代码
package mainimport "fmt"func main() {fmt.Println("Hello world")
}
- runtime创建最初的线程m0和goroutine g0,并把2者关联。
- 调度器初始化:初始化m0、栈、垃圾回收,以及创建和初始化由GOMAXPROCS个P构成的P列表。
- 示例代码中的main函数是main.main,runtime中也有1个main函数——runtime.main,代码经过编译后,runtime.main会调用main.main,程序启动时会为runtime.main创建goroutine,称它为main goroutine吧,然后把main goroutine加入到P的本地队列。
- 启动m0,m0已经绑定了P,会从P的本地队列获取G,获取到main goroutine。
- G拥有栈,M根据G中的栈信息和调度信息设置运行环境
- M运行G
- G退出,再次回到M获取可运行的G,这样重复下去,直到main.main退出,runtime.main执行Defer和Panic处理,或调用runtime.exit退出程序。
调度器的生命周期几乎占满了一个Go程序的一生,runtime.main
的goroutine执行之前都是为调度器做准备工作,runtime.main
的goroutine运行,才是调度器的真正开始,直到runtime.main
结束而结束。
8.Sysmon有什么作用
sysmon是一个管理线程或者说守护线程,其是对GMP调度架构的补充和兜底。
GMP的调度完全是主动协作式的调度。主动协作式的调度性能很高,但是在某些情况下会出现单个goroutine长期占据时间片甚至一直占据时间片的情况。
比如:
- 某个goroutine不执行主动调度、不调用系统调用、不做函数调用,就会一直运行直到goroutine退出;
- 某个goroutine处于syscall状态时也无法触发主动调度,可能会造成该goroutine长时间占据时间片;
sysmon的作用就是处理类似上面情况,其主要的工作内容有:
- 定期查看netpoll有无就绪的任务,防止netpoll阻塞队列中的goroutine饥饿;
- 定期查看是否有p长时间(10ms)处于syscall状态,如有则将p的持有权释放以执行其他g;
- 定期查看是否有p长时间(10ms)没有调度,如有则对当前m发送信号,触发基于信号的异步抢占调度;
9.可视化GMP编程
方式1:go tool trace
方式2:Debug trace
参考:2、Golang的协程调度器原理及GMP设计思想 · 语雀