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() 触发流程
go func(){}`创建一个新的`goroutine
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