go的并发实现采用的是M:N的线程模型,落地就是gmp模型。
M:N模型如下图:
gmp模型如下图:
---
Go 的 GMP 模型是其 高效并发调度机制的核心。GMP 代表:
-
G:Goroutine(用户态线程)
-
M:Machine(绑定内核线程)
-
P:Processor(调度器/执行上下文)
Go 通过这三个组件,实现了 goroutine 的调度和执行,避免了频繁的线程创建与上下文切换,性能优秀。
那么gmp模型是怎么实现的呢?
多个 Goroutine (G) -> 由 P 管理调度 -> 由 M(线程)实际执行;
具体来,需要执行的G是放在P的队列里面等着被执行调用的,不过有时候会有一些岔子,为了保证M的使用率,会有一些具体的调度算法,让G被调来调取,大概情况是:
-
Hand off:比较重的调度;M阻塞了(syscall),就把M手头的G收走,让其他M去执行;
-
Work Stealing:M对于的P中的队列没有G了,从其他地方调一些来
-
普通调度:G1阻塞了,例如sleep,io了,直接把G1挂起,让其他G被M执行。
-
等等
head off可以用图片来理解:
Hand off图源
整体的调度思路可以用伪代码来理解:
// G = Goroutine,代表一个用户级线程(任务)// M = Machine,代表一个工作线程(对应一个内核线程)// P = Processor,代表执行资源(运行队列+执行上下文),M 必须绑定 P 才能运行 Gtype G struct {fn func() // Goroutine 要执行的函数}type M struct {p *P // 当前绑定的 Pcurg *G // 当前正在执行的 G// ... 还有调用栈等}type P struct {runQueue []*G // 本地 G 队列// 还包括调度器上下文、调度时间等}// 系统初始化时,创建 GOMAXPROCS 个 P,通常等于 CPU 核数func initRuntime() {for i := 0; i < GOMAXPROCS; i++ {allP[i] = new(P)}// 启动第一个 MstartM()}// 启动一个 M(内核线程),从全局找可用的 P,然后调度func startM() {m := new(M)m.p = acquireP() // 找一个空闲 Pgo m.run() // 启动内核线程,进入调度循环}// M 的主循环,持续运行 Gfunc (m *M) run() {for {g := m.p.findRunnableG() // 找到一个可运行的 Gif g == nil {// 若本地队列空了,可以尝试 steal 其他 P 的 Gg = stealFromOtherP()if g == nil {// 若仍找不到,当前 M 休眠stopM(m)return}}m.curg = grunG(g) // 运行 G 的函数m.curg = nil}}// P 的调度器,从本地 runQueue 中找 goroutinefunc (p *P) findRunnableG() *G {if len(p.runQueue) == 0 {return nil}g := p.runQueue[0]p.runQueue = p.runQueue[1:]return g}// 当调用 go f() 时,生成一个新的 G,并放入当前 P 的队列func goNew(f func()) {g := &G{fn: f}curP := currentM().pcurP.runQueue = append(curP.runQueue, g)// 若当前 M 忙不过来,可触发 newM 让新线程帮忙跑 G}
参考资料:
深入浅出 Go 语言 GMP 模型 https://juejin.cn/post/7434518199234740233
刘丹冰 【Golang深入理解GPM模型】https://www.bilibili.com/video/BV19r4y1w7Nx/?share_source=copy_web&vd_source=4ab2dac702abaae48d1782021ca7150c