GMP 模型
GMP模型的原理不再多讲,处理器(P)通过维护自己的队列拿到需要执行的协程(G)并绑定到线程(M)。常见的GMP知识点不外乎全局队列,M0G0的知识,工作偷取和P遇到自旋M就解绑定并寻找新的M。
需要注意的一点是G,M与P的数量。G的数量理论上是无限的,但与程序自身的内存有关;P的数量是runtime.GOMAXPROCS
控制的,注意这里是运行时宏;而M的数量就是操作系统相关的知识了,也同样受到资源的限制(题外话,进程数量受什么控制?)。
main, 等等我的 Goroutine
我们知道 Go 的一切都是 Goroutine 在跑,那么我们的主函数实际上也是跑在一个 Goroutine 里的,只不过这个Goroutine比较特殊,被标记为main Goroutine(废话)。那么我们用户创建这些 Goroutine 何时退出呢?
进程被 Kill / 进程崩溃
显然这个时候所有的 G 都会退出,毕竟系统资源都没了。
main函数结束
这个时候所有的 G 的资源也会被收回。
主动退出
正常执行完毕 / return / 使用 Context, 这个比较复杂之后特地讲 / panic(不要用)
然而main Goroutine 可不会在乎你创建的 G 是否执行完了。怎么确保我们创建的G能够执行完再让 main G退出呢?
channel
对于一个比较简单的G,我们直接创建一个无缓冲区管道阻塞一下即可。
1 | func main() { |
显然这种办法比较低级,如果我需要开启不定数量的 G,这种办法就不好用了(吗?实际上开一个有缓冲区的channel也可以,而且能控制同时运行的 G 的数量)。
sync.Waitgroup
这是官方给我们用来控制并发流程的。
1 | import "sync" |
Goroutine 更详细的顺序
运行一下这段代码:
1 | func main() { |
我们控制P的数量是1,并看似从0到9创建了一堆G。但实际上,程序的输出是:
1 | 9 |
哇,太棒了。为什么会这样?这就必须讲一下 GMP 模型中的三个队列了。
三个队列
每个P所拥有的队列大概是这样:runnext
,local
。以及程序本身有一个global
。
每次创建G的时候,G会首先抢占 runnext
。这确保了如果有很多P的话,G会被优先执行,不会有空闲的P,这太浪费资源了。runnext
只有一个空位,从名字也能看懂这是P要绑定给M的那个G。但如果有一个新来的G呢?old G就会被new G挤走,到local
队列的末尾,下一个调用。这也是为了每个G都能被执行。
local
队列最长256,是一个环形队列,头尾指针控制。如果这玩意也满了G就会去global
。当然如果空了就会偷取,不再多谈。
因此这样输出什么情况就很明显了,因为IO操作很慢,因此最后出现的G也就是9抢占了runnext
进行执行,而剩下的G则以0~8的方式从local
中取出来执行,就有了上面的输出。