这里写自定义目录标题
Go 的并发机制
4.1 原理探究
Go 其实时在操作系统提供的内核线程之上,搭建了一个特有的两级线程模型。
goroutine 的含义:不要用共享内存的方式来通信,作为替代,应该以通信作为手段来共享内存。
- Go 使用 channel 在多个 goroutine 之间传递数据,并且会保证整个过程的并发安全性。
4.1.1 线程实现模型
核心元素:
- M:一个 M 代表一个内核线程,或者说“工作线程”
- P:一个 P 代表执行一个 Go 代码片段所必需的资源(“上下文环境”)
- G:goroutine 的缩写
M、P、G之间的关系
- 一个 G 的执行需要 P 和 M 的支持。一个 M 和 一个 P 关联之后,就形成了一个有效的 G 运行环境(内核线程+上下文环境)
- 每个 P 都会包含一个可运行的 G 的队列,该队列中的 G 会依次传递给本地 P 关联的 M,获得运行时机。
从宏观来看,他们之间的联系可以如下图所示:
如果将焦点扩大一点,从内核+用户空间层面看:
- 可以看到,M 与 KSE 总是一对一的关系,一个 M 能且仅能代表一个内核线程。
- M 与 P 之间也总是一对一的,而 P 与 G 之间则是一对多的关系(队列)
- M 与 G 之间也会建立关联,因为一个 G 终归会由一个 M 来负责运行,两者通过 P 来牵线
注:上图只作为一般性的示意,这三者之间的关系在调度过程中会多变
接下来,继续
1. M
一般来说,创建一个 M,都是因为没有足够的 M 来关联 P 并运行其中可运行的 G。不过,在运行时系统执行系统监控或者垃圾回收等任务的时候,也会导致 M 的创建。
M的部分结构如下:
- g0:是一个特殊的 goroutine,是 Go 运行时系统在启动之初创建的,用于执行一些运行时任务
- mstartfn:表示 M 的起始函数,就是go 携带的那个
- curg:存放当前 M 正在运行的那个 G 的指针
- p:指向与当前 M 相关联的那个 P
- nextp:用于暂存与当前 M 有潜在关联的 P,赋值即预联(M 重启时,会将 nextp 赋值为 p)
- spinning:表示这个 M 是否正在寻找可运行的 G(寻找过程中 M 处于自旋状态)
- lockedg:表示当前 M 锁定的那个 G(如果有的话,一旦锁定,这个 M 就只能运行这个 G)
1.1 M 的创建与停止
创建与执行
- M 在创建之初,会被加入全局的 M 列表中。此时,它的起始函数和预联的 P 也会被设置。
- 运行时系统,会为这个 M 专门创建一个新的内核线程,并与之相关联。
- M创建之后,Go 运行时系统会先对其进行初始化:栈空间、信号处理等。初始化完成之后,M 的起始函数将会执行。(如果起始函数是系统监控任务的话,M 会一直执行它,不会进行后面的流程)
- 起始函数执行完成之后,M 会与预联的 P 完成关联
停止
系统停止 M 的时候,会把它放入调度器的空闲 M 列表(运行时系统会优先从这个列表获取)
一般来说,可以认为 Go 本身对于线程数量(10000)没有限制。因为操作系统内核对进程的虚拟内存的布局控制以及大小限制,一般难以共存如此量级的线程。
2. P
P 的最大数量设置
- 调用函数 runtime.GOMAXPROCS 设置数量
- 最好在Go 程序的 main 函数的最前面调用 runtime.GOMAXPROCS(因为这个函数调用的执行会暂时让所有的 P 都脱离运行状态,并试图阻止任何用户级别的 G 的运行)
- M 的数量一般会比 P 多
原因:当 M 因为系统调用而阻塞的时候,运行时系统会把该 M 和与之关联的 P 分离开来。如果这时候 P 的可运行 G 队列中还有未被运行的 G,那么运行时系统会找到一个空闲 M(或者创建一个新的 M),并与该 P 关联以满足这些 G 的运行需要。 - P 的最大数量确定之后,运行时系统会重整全局的 P 列表,调度放到某个 P 的可运行 G 队列中
- 当一个 P 的可运行 G 列表为空的时候,会被放入空闲 P 列表(比如说重整全局的 P 列表的时候,P 被清空可运行 G 队列之后,才会被放入空闲 P 列表)
P 的状态转换
- Pidle:当前 P 未与任何 M 存在关联
- Prunning:当前 P 正在与某个 M 关联
- Psyscall:当前 P 中的运行的那个 G 正在进行系统调用
- Pgcstop:表明运行时系统需要停止调度(运行时系统在开始垃圾回收的某些步骤前,就会试图把全局 P 列表中的所有 P 都置于此状态)
- Pdead:当前 P 已经不会再被使用(只能被销毁)
3. G
G 的状态
- Gidle:当前 G 刚被新分配,但还未初始化
- Grunnable:当前 G 正在可运行队列中等待运行
- Grunning:当前 G 正在运行
- Gsyscall:当前 G 正在执行某个系统调用
- Gwaiting:当前 G 正在阻塞
- Gdead:当前 G 正在闲置(可以重新初始化并使用)
- Gcopystack:当前 G 的栈正被移动,移动的原因可能是栈的扩展或收缩
在GC 扫描时,会发生一些组合状态,如:Gscanrunning 等
4. 核心元素的容器
本节,我们一直是将实现和操纵 Go 的线程实现模型的内部程序笼统地称为“运行时系统”。实际上,它可以更明确地称为”调度器“,我们下一节再详细讲述。