CSP 思想
CSP,Communicating Sequential Processes,通信顺序进程。
CSP 思想是以通信实现共享内存,用 Channel 来传递消息,Process 用于执行。
GPM 并发模型
Go 并发模型吸收了 CSP 模型的思想,采用 channel 和 goroutine 来实现。
有关于 channel 和 goroutine 部分详见 Go 并发基础。
吸收二级线程模型经验进行改良,关于线程模型详见 进程与线程。
先上一张总的 goroutine 调度模型图。
四大要素
在 GPM 模型中,当一个 goroutine 到来时一般先放到全局运行队列(Global Run Queue,GRQ)中,此外还有已经绑定到 P 上的本地运行队列(Local Run Queue,LRQ),由调度器 Sched 从 GRQ 中获取分配。
- G 是 goroutine 的缩写,G 是对 goroutine 的抽象描述,一个结构体,并不是原原本本的 goroutine。G 中存放运行栈、状态、任务函数等。
- G 的切换不涉及用户态到内核态的切换,goroutine 上下文切换只涉及 3 个寄存器(PC 程序计数器 / SP 栈指针寄存器 / DX 通用寄存器)的值修改,以及 16 个寄存器的刷新。
- P 是 Processer 的缩写,代表逻辑处理器。对 G 来说 P 相当于 CPU 核,G 只有绑定到 P 上才能被调度;对于 M 来说,P 提供相关的执行环境 Context,如内存分配状态 mcache,任务队列 Goroutine Quene。
- P 的数量由 runtime.GOMAXPROCS 设置,但是不论设置为多大,P 的数量最大为 256。
- M 是 machine 的缩写,内核线程的抽象,代表真正执行计算的资源。在绑定 P 之后进入 schedule 循环。
- 数量不定,由 Go runtime 调整,为防止创建过多的内核线程而导致调度压力过大,目前默认最大限制为 10000 个。
- M 不保存 G 的状态,这是 G 可以跨 M 调度的基础。
- Sched:Go 调度器,维护 M 队列,各种 G 队列以及调度器本身的一些状态信息。
四者关系
- G 会被分配到各个 P 的 LRQ 中。
- 当且仅当 M 获得一个 P 后才可以执行 G。
- 空闲的 M 可以和空闲的 P 结合,然后被 Sched 调度执行 LRQ 上的 G。
Sched 调度策略
任务窃取
既然 P 是决定并发执行 G 的关键,那么就尽量不要让 P 空闲着。
Sched 会协调每个 P 维护的 LRQ 上 G 的数量,如从 GRQ 分配 G 到 P 的 LRQ 上,从负担重的 LRQ 上截取部分 G 到负担轻的 P 上。
发生阻塞
在 Go 中阻塞主要分为 4 种情况,两大类。
阻塞了 G
- 由于原子操作、互斥量或通道操作调用导致 G 阻塞,Sched 会把阻塞的 G 切换出去执行 LRQ 上其他的 G。
- 由于网络请求和 I/O 操作导致 G 阻塞。由 Go 提供的网络轮询器(NetPoller)通过 kqueue(MacOS),epoll(Linux)或 iocp(Windows)来实现 I/O 多路复用。
- G1 进行网络系统调用而被移动到网络轮询器并且处理异步网络系统调用,Sched 让 M 从 LRQ 中选择 G2 继续执行;当 G1 执行完毕后会被重新放回 LRQ 中。
阻塞了 M
- G 执行 sleep 阻塞了 M,由 Go 后台一个监控线程 sysmon 监控长时间运行的 G,标识它可以被抢占,让别的 G 先执行(在 Go 语言中,sysmon 会用于检测抢占。sysmon 是 Go 的 Runtime 的系统检测器,sysmon 可进行 forcegc、netpoll、retake 等一系列骚操作)。
- G 进行系统调用阻塞了 M,NetPoller 无法使用。P 和 M 分离寻找其他的 M 继续执行,M 带着 G 一起等待系统调用完成后让 G 回到 LRQ,自己被回收或空闲着等别的 P。
优点
- G 切换发生在用户空间,不发生用户态与内核态的切换。
- M 与 P 解耦,P 抛弃阻塞的 M 寻找别的空闲 M,提高并发效率。
小贴士
注:可以查阅 CPU、内核与逻辑处理器的关系。
P 与这里的逻辑处理器不是一回事。