goroutine与线程的关系

前言

学到这里,我们需要了解一下OS线程一般都是有属于自己的线程栈内存(通常为2MB)。一个goroutine栈在刚启动的时候确实很小的(通常是2KB),所以一次性创建10w个goroutine也是没有问题的,随着运行goroutine栈最大可达到1GB。goroutine属于用户态线程,go语言在runtime层面通过实现了一个GPM机制来对goroutine进行调度。

goroutine的调度机制

首先来大致解释下什么是GPM

G:就是goroutine,包含goroutine信息以及与所在P的绑定等信息

P:管理着一组goroutine的队列,P里面会存储当前goroutine运行的上下文环境(函数指针,堆栈地址以及地址边界),P内部对自己管理的一些列G进行调度,当自己管理的队列消费完了还会去全局队列中取,如果全局队列也消费完了还会去其他的P中取任务

M:是Golang中对os内核线程的抽象,M与内核线程一般就是11对应的关系,goroutine最终都是会在M上进行执行的

我们来稍微理解一下这个GPM模型,本人会觉得与java中的线程池模型还是很相似的,只是本人研究的java线程池中并无全局队列以及去其他队列抢任务的概念出现,了解jJAVA中线程池工作原理戳这里。在GPM中,P和M通常也是11对应的,但这并不是绝对的,可以这么通俗的来理解一个队列P对应一个os线程M,os线程由操作系统cpu运行,然后P来对队列中的任务goroutine进行调度,让每一个goroutine在os线程上不停的来回切换运行(准确一点的表达:P管理着一组G挂载在M上运行,当一个G长久阻塞在一个M上时,runtime会新建一个M,阻塞G所在的P会把其他的G挂载在新建的M上去。当旧的G阻塞完成或者认为其已经死掉时那么就会回收旧的M)。

GOMAXPROCS

​ 在前言中我们说了G的内存大小以及创建数量等问题,那么P由这方面的限制吗?P的个数是通过runtime.GOMAXPROCS设定(最大256),Go1.5版本之后默认为物理线程数。在并发量大的时候会适当增加一些P和M,但是也不会太多,切换太频繁也没有太多必要。

实例查看效果

Go运行时的调度器使用GOMAXPROCS参数来确定需要使用多少个OS线程来同时执行Go代码。默认值是机器上的CPU核心数。例如在一个8核cpu上,调度器会把Go代码同时调度到8个OS线程上(GOMAXPROCS是m:n调度中的n)。

Go语言中可以通过runtime.GOMAXPROCS()函数设置当前程序并发时占用的CPU逻辑核心数。

Go1.5版本之前,默认使用的是单核心执行。Go1.5版本之后,默认使用全部的CPU逻辑核心数。

我们可以通过将任务分配到不同的CPU逻辑核心上实现并行的效果,这里我们来举个例子看看。

例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package main

import (
"fmt"
"runtime"
"time"
)

func a() {
for i := 1; i < 10; i++ {
fmt.Println("A:", i)
}
}

func b() {
for i := 1; i < 10; i++ {
fmt.Println("B:", i)
}
}

func main() {
runtime.GOMAXPROCS(1)
go a()
go b()
time.Sleep(time.Second)
}
运行结果:
modora#: go run ./main.go
B: 1
B: 2
B: 3
B: 4
B: 5
B: 6
B: 7
B: 8
B: 9
A: 1
A: 2
A: 3
A: 4
A: 5
A: 6
A: 7
A: 8
A: 9

上面例子中我们设置了runtime.GOMAXPROCS()为1,也就是说GPM模型中最多一个OS线程在运行,可以很明显的看到Agoroutine和Bgoroutine是串行打印,原因也很简单,两个goroutine在一个OS线程上进行调度的。下面我们将runtime.GOMAXPROCS()设置成2,两个goroutine在两个OS线程上进行调度,这里其实可以理解在OS线程层面并行。

例2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package main

import (
"fmt"
"runtime"
"time"
)

func a() {
for i := 1; i < 10; i++ {
fmt.Println("A:", i)
}
}

func b() {
for i := 1; i < 10; i++ {
fmt.Println("B:", i)
}
}

func main() {
runtime.GOMAXPROCS(2)
go a()
go b()
time.Sleep(time.Second)
}
执行结果:
modora#: go run ./main.go
A: 1
A: 2
A: 3
A: 4
A: 5
A: 6
A: 7
A: 8
B: 1
B: 2
B: 3
B: 4
B: 5
B: 6
B: 7
B: 8
B: 9
A: 9

再次运行上述代码,发现AB变成了交替打印。各种意味自行理解哈~