解释GMP模型
GMP 模型是 Go 语言调度器采用的并发编程模型,它包含三个重要的组件:Goroutine(G)、逻辑处理器(P)和操作系统线程(M)。这些组件协同工作以实现 Go 程序的高效并发执行。
具体来说,
- Goroutine (G) 是 Go 语言中轻量级的并发执行单元,类似于线程但比线程更小、更灵活。每个 goroutine 都有自己独立的堆栈和寄存器等信息,可以通过 go 关键字创建并发执行任务。
- 逻辑处理器(P)是一个虚拟的执行单元,负责调度 goroutine 和执行 Go 代码。Go 程序中有多个 P,每个 P 可以运行多个 goroutine,因此可以实现真正的并发执行。
- 操作系统线程(M)是实际的执行单元,负责将 goroutine 调度到逻辑处理器上执行。Go 程序中通常会创建多个 M,以便在多核 CPU 上实现并发执行。
GMP的调度流程
GMP 调度器采用抢占式的协作调度,具体调度流程如下:
- 主线程启动,在主线程中创建一个操作系统线程(M)和一个逻辑处理器(P)。
- 当有 goroutine 函数被调用时,它会被放入到一个全局队列中等待执行。
- P 从全局队列中获取任务并执行。如果 P 执行的 goroutine 阻塞(例如在等待 I/O 完成),则该 P 的所有 goroutine 都会被暂停,P 会将自己标记为阻塞状态并开始寻找其他可用的 P。
- 如果没有可用的 P,则 M 变为自由线程,并且会去创建一个新的 P,以便执行未完成的 goroutine。新的 P 将加入到一个全局 P 列表中,而 M 将继续尝试在列表中寻找可用的 P。
- 当 goroutine 阻塞时,Goroutine 在堆上分配一块内存来保存其状态,并被添加到相关的等待队列中。而主线程会进入休眠状态,等待唤醒事件发生。
- 当阻塞的 goroutine 可以继续执行时,调度器会将它从等待队列中移除,并将其重新添加到全局队列中,等待 P 来执行。
- 当程序结束时,所有未完成的 goroutine 都会被杀死,而 P 和 M 也会被回收。
P和M的个数
$GOMAXPROCSruntimeGOMAXPROCS()$GOMAXPROCSM 与 P 的数量没有绝对关系,一个 M 阻塞,P 就会去创建或者切换另一个 M,所以,即使 P 的默认数量是 1,也有可能会创建很多个 M 出来。
P和M何时会被创建
P: 在确定了 P 的最大数量 n 后,运行时系统会根据这个数量创建 n 个 P。
M: 没有足够的 M 来关联 P 并运行其中的可运行的 G 时创建。比如所有的 M 此时都阻塞住了,而 P 中还有很多就绪任务,就会去寻找空闲的 M,而没有空闲的,就会去创建新的 M。
goroutine创建流程
gogo具体的创建流程如下:
go值得注意的是,Go 语言的调度器可以同时运行数百万个 goroutine,因此不必担心创建大量的 goroutine 会导致系统负荷过重。但是,如果你的程序中存在频繁创建和销毁 goroutine 的情况,应该考虑使用 sync.Pool 等技术来优化内存分配和回收效率。
goroutine什么时候会被挂起
goroutine 会在以下情况下被挂起:
- 发生阻塞,例如等待 I/O 操作的完成或者发送或接收通道上的数据时没有可用的对等方。
- 发生调用 runtime.Gosched(),让出 CPU 给其他 goroutine 执行。
- 发生同步操作,例如 sync.Mutex 或 sync.WaitGroup 的锁定和解锁操作。
- 发生垃圾回收(GC)。
- 发生错误,例如 panic 或者超时。
同时启动了一万个goroutine,会如何调度
一万个G会按照P的设定个数,尽量平均地分配到每个P的本地队列中。如果所有本地队列都满了,那么剩余的G则会分配到GMP的全局队列上。接下来便开始执行GMP模型的调度策略:
- 本地队列轮转:每个P维护着一个包含G的队列,不考虑G进入系统调用或IO操作的情况下,P周期性的将G调度到M中执行,执行一小段时间,将上下文保存下来,然后将G放到队列尾部,然后从队首中重新取出一个G进行调度。
- 系统调用:P的个数默认等于CPU核数,每个M必须持有一个P才可以执行G,一般情况下M的个数会略大于P的个数,这多出来的M将会在G产生系统调用时发挥作用。当该G即将进入系统调用时,对应的M由于陷入系统调用而进被阻塞,将释放P,进而某个空闲的M1获取P,继续执行P队列中剩下的G。
- 工作量窃取:多个P中维护的G队列有可能是不均衡的,当某个P已经将G全部执行完,然后去查询全局队列,全局队列中也没有新的G,而另一个M中队列中还有3很多G待运行。此时,空闲的P会将其他P中的G偷取一部分过来,一般每次偷取一半。
goroutine内存泄漏原因和处理
原因:
Goroutine 是轻量级线程,需要维护执行用户代码的上下文信息。在运行过程中也需要消耗一定的内存来保存这类信息,而这些内存在目前版本的 Go 中是不会被释放的。因此,如果一个程序持续不断地产生新的 goroutine、且不结束已经创建的 goroutine 并复用这部分内存,就会造成内存泄漏的现象。造成泄露的大多数原因有以下三种:
- Goroutine 内正在进行 channel/mutex 等读写操作,但由于逻辑问题,某些情况下会被一直阻塞。
- Goroutine 内的业务逻辑进入死循环,资源一直无法释放。
- Goroutine 内的业务逻辑进入长时间等待,有不断新增的 Goroutine 进入等待。
解决方法:
- 使用channel
- 1、使用channel接收业务完成的通知
- 2、业务执行阻塞超过设定的超时时间,就会触发超时退出
- 使用pprof排查
- pprof是由 Go 官方提供的可用于收集程序运行时报告的工具,其中包含 CPU、内存等信息。当然,也可以获取运行时 goroutine 堆栈信息。
- 使用 context 进行超时控制:context 包提供了超时控制等功能,可以避免 goroutine 在执行过程中发生死循环等问题。
- 利用 Go 的垃圾回收机制:Go 运行时系统包含了自动垃圾回收机制,可以周期性地检查并回收不再使用的内存。