抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

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
2
3
4
5
6
7
8
9
10
func main() {
ch := make(chan interface{})

go func() {
time.Sleep(5 * time.Second)
ch <- "Done"
}()

data := <-ch
}

显然这种办法比较低级,如果我需要开启不定数量的 G,这种办法就不好用了(吗?实际上开一个有缓冲区的channel也可以,而且能控制同时运行的 G 的数量)。

sync.Waitgroup

这是官方给我们用来控制并发流程的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import "sync"

func main() {
var wg sync.WaitGroup // 声明这个结构, 如果要传到函数里记得指针

// wg.Add(100) Add其实就是要执行的任务数量,直接在这加也行
for i := 0; i < 100; i++ {
wg.Add(1)
go func() {
// [注意] wg.Add(1) 不能写在这,写了 linter 也会报错的
// 因为可能当前还没有 wg.Add(1), wg.Wait() 的逻辑已经通过了

// 函数逻辑
wg.Done() // 做好了
}()
}

wg.Wait() // 等待结束
}

Goroutine 更详细的顺序

运行一下这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
func main() {
runtime.GOMAXPROCS(1)
for i := 0; i < 10; i++ {
i := i
go func() {
fmt.Println(i)
}()
}

var ch = make(chan int)
<- ch // 这里阻塞会 fatal error, 因为死锁了
}

我们控制P的数量是1,并看似从0到9创建了一堆G。但实际上,程序的输出是:

1
2
3
4
5
6
7
8
9
10
9
0
1
2
3
4
5
6
7
8

哇,太棒了。为什么会这样?这就必须讲一下 GMP 模型中的三个队列了。

三个队列

每个P所拥有的队列大概是这样:runnextlocal。以及程序本身有一个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中取出来执行,就有了上面的输出。

评论