Golang并发(2)
Golang 并发(二)
参考 链接:https://juejin.cn/post/7075723579303657485
GMP 模型
Goroutine 是 Go 语言中的协程,实现了轻量级并发,和传统的线程相比,Goroutine 有以下特点:
- 轻量级
- 高效调度:Go 运行时在用户态进行调度,避免了频繁的上下文切换带来的开销,使得调度更加高效
GMP 模型基本概念
G: Goroutine
M: Machine
P: Processer
Goroutine 是 Go 语言中的协程,代表一个独立的执行单元,Goroutine 比线程更加轻量级,启动一个 Goroutine 的开销非常小,Goroutine 的调度由 Go 运行时在用户态进行。
Machine: 代表操作系统的线程,实际执行 Go 代码,一个 M 可以执行多个 Goroutine,但是同一时间只能执行一个 Goroutine。M 和操作系统的线程直接对应,Go 运行时通过 M 来利用多核 CPU 的并行计算能力。
Processer: P 代表执行上下文,管理者可运行的 Goroutine 队列,并且负责和 M 进行绑定,P 的数量决定了可以并行执行 Goroutine 的数量。Go 运行时会根据系统的 CPU 核数设置 P 的数量。
在 GMP 模式中,线程是物理生产者,调度器会将 goroutine 分派给一个线程。
说明:
- 全局队列:要执行 goroutines 的队列
- P 本地队列: 和全局队列一样,它包含要执行的 goroutine,但是这个队列的最大容量是 256.当本地队列满的时候,会将 goroutie 的一般发送到全局队列,最后 G 创建的 goroutine 将在同一个队列中排队,确保本地关联
- P 列表: 一旦确定 GOMAXPROCS 的值,所有 P 的列表都将会被创建
- M 正在运行的线程: 从 P 的本地队列获取任务,如果该队列为空,M 将从全局队列获取 goroutines 到 P 的本地队列,或者从其他的 P 的队列获取 goroutines。G 在 M 中运行,当任务完成的时候,它将继续拉一个新的 G
MP 创建周期
- P: 在确定 P 的数量后,(runtime)运行时将创建 P。
- M: 如果没有足够的 M 来执行 P 的任务,它将被创建。(所有 M 都被阻止,将创建新的 M 来运行 P 的任务)
调度器机制
调度过程
- 首先创建一个 G 对象,然后 G 被保存在 P 的本地队列或者全局队列(global queue),这时 P 会唤醒一个 M。
- P 按照它的执行顺序继续执行任务。M 寻找一个空闲的 P,如果找得到,将 G 移动到它自己。
- 然后 M 执行一个调度循环:调用 G 对象->执行->清理线程->继续寻找 Goroutine。
调度策略
- Work Stealing 机制:当本线程没有可执行的 G 时,优先从全局 G 队列中获取一批 G。如果全局队列中没有,则尝试从其他 P 的 G 队列中偷取 G。
- Hand Off 机制:当本线程因 G 进行系统调用等阻塞时,线程会释放绑定的 P,把 P 转移给其他空闲的 M 执行。
Concurrency(并发):至少有更多的 GoMaxProc goroutines 同时运行。但是,如果 GOMACPROCS<CPU 内核,它还设置了并发限制。
Preemptive(抢占):协同程序必须放弃 cpu 时间。但是在 golang,一个 goroutine 每次最多可以运行 10 毫秒,以避免其他 goroutine 饥饿。
Global Goroutines Queue(全局 goroutine 队列):当工作窃取失败时,M 可以从全局队列中拉出 goroutines。
goroutine 实现
go func() 触发流程
|
|
G 保存在 P 的本地队列,如果本地队列满了,保存在全局队列
G 在 M 上运行,每个 M 绑定一个 P。如果 P 的本地队列没有 G,M 会从其他 P 的本地队列,挥着 G 的全局队列,窃取 G
当 M 阻塞时,会将 M 从 P 解除。把 G 运行在其他空闲的 M 或者创建新的 M
当 M 恢复时,会尝试获得一个空闲的 P。如果没有 P 空闲,M 会休眠,G 会放到全局队列
生命周期
流程说明
- runtime 创建 M0,G0
- 调度器初始化:初始化 M0,栈,垃圾回收,创建初始的长度为
GOMAXPROCS
的 P 列表 runtime.main
创建代码的main.main
,创建主 goroutine,然后放到 P 的本地队列- 启动 M0, M0 绑定 P
- 根据 goroutine 的栈和调度信息,M0 设置运行环境
- 在 M 中运行 G
- G 退出,
runtime.main
调用defer
,panic
,最后调用runtime.exit