🎗️Golang GMP模型
type
status
date
slug
summary
tags
category
icon
password
Blocked by
Blocking
AI summary
进程、线程、协程的概念
进程:进程是操作系统资源分配的最小单元
线程:操作系统内核视角下最小的调度单元,其创建、销毁、调度都是需要内核参与
协程是一个轻量级的线程。协程将程序中的一段代码块打包,可以快速在协程间切换,支持高并发。在go中可轻松的起上千个协程。
- 协程用来精细化利用线程资源,提升系统资源利用率。
- 从runtime的角度出发,协程是一个g结构体,包含了协程的运行状态和运行栈
- 从线程的角度出发,协程是一段打包的程序段,自带执行现场,我线程来执行它
goroutine:golang是一门天生支持协程的语言,其goroutine就是golang中的协程,相比于原生的协程,具备如下核心优势:
- g可以与p、m动态结合,整个调度过程有很高的灵活性
- g的栈空间可以动态阔所容,既能做到使用方便,也尽可能节省资源
golang是一门天生支持高并发的语言,也是天生支持协程的语言,其goroutine就是golang中的协程,相比于原生的协程,goroutine的栈空间可以动态阔所容,既能做到使用方便,也尽可能节省资源,其次go在 runtime的层面实现了GMP模型(任务调度模型),g可以与p、m动态结合,整个调度过程有很高的灵活性。
GMP的架构

GMP模型是
一个任务调度系统
,m是这个调度系统中的引擎,当m与p结合时,m不断交替执行g0和gg
- g即 goroutine,是golang中协程的抽象
- g有自己的运行栈、生命周期状态、以及执行的任务函数(go func指定的func)
- g需要绑定在m上执行,在g的视角下,m就是它的cpu
m
- m即machine,是go对线程的抽象
- m需要与p进行结合,从而进入gmp调度体系中
- m的运行目标始终在g0和g之间切换
g与g0是始终交替执行:g0负责检索任务列表中需要执行的任务;g是g0找到并分配给m执行的一个具体任务
p
- p processor是golang的调度器
- p可以理解为m的执行代理,m需要与p绑定后,才会进入gmp调度模式中;因此p的数量决定了g的最大并行数量,其中p的大小可以由用户设置 GOMAXPROCS 进行设定
- p是g的存储容器,其自带一个本地队列,承载着一系列等待被调度的g
同时go的并发工具(channel、mutex等)均契合GMP作了适配,保证在执行阻塞时,会将阻塞颗粒度限制在g并非m的粒度,使得阻塞与唤醒都是用户态行为,无需内核介入,同时一个g的阻塞不会影响m下其他g的运行
在设计io模型时,go采用了linux系统提供的epoll多路复用技术,而为了避免因为epoll_wait操作引起的m力度阻塞,golang专门设计了一套netpoll机制,使用用户态的gppark指令实现阻塞操作,使用用户态的goready指令实现唤醒操作,从而将io行为控制在g的粒度
create one goroutine的过程
当某个 g 中通过 go func(){...} 操作创建子 g 时,会先尝试将子 g 添加到当前所在 p 的 lrq 中(无锁化);如果 lrq 满了,则会将 g 追加到 grq 中(全局锁). 此处采取的思路是
“就近原则”
find runnable goroutine的过程
- 每经过61次调度,需要先处理一次全局队列,避免产生饥饿问题
- 尝试从本地队列中获取g
- 尝试从全局队列中获取g
- 尝试通过netpoll,从io就绪队列中获取g
- 尝试随机,从其他本地队列中窃取其一半的g
- 若没找到g,将p置为idel,添加到shedtl pidel队列(动态缩容)
- 若m无事可做,将其加入shedtl idel 队列(动态缩容)
- p放入 shedtl pidel 队列
- m放入 shedtl midle队列
- 停止m的运行,避免产生资源浪费
让渡的设计

结束让渡

当g执行结束时,会正常退出,并将执行权交给g0
- g运行结束后,调用
goexit1
方法,通过mcall 指令切换至g0,由g0调用goexit0方法,由g0执行一下步骤 - 将g状态由running 更新为dead
- 清空g中的数据
- 解除g和m的关系
- 将g添加到p的gfree队列,以供复用
- 调用schedule方法发起一轮新调度
主动让渡

主动让渡是指用户手动调用
runtime.Gosched方法
,让出g持有的执行权,在Gosched方法中,后通过mcall切换指令到g0,并由g0执行gosched_m方法- 将g的状态由running更新为runnable
- 接触g和m的关系
- 将g直接添加到全局队列中
- 调用schedule方法发起一轮新调度
阻塞让渡
阻塞让渡是指g的执行过程中所依赖的外部条件没有达到,需要进入阻塞等待的状态(waiting),直到条件达成后才能进入runnable状态
go针对mutex、channel等并发工具的设计,在底层都是采用阻塞让渡的设计模式,具体执行的方法位于runtime/proc.go的gopark方法
- 通过mcall从g切换到g0,并有g0执行park_m方法
- g0将g由running更新为waiting状态,发起一轮新的调度
此处需要注意,在阻塞让渡后,g 不会进入到 lrq 或 grq 中,因为 lrq/grq 属于就绪队列. 在执行 gopark 时,使用方有义务自行维护 g 的引用,并在外部条件就绪时,通过 goready 操作将其更新为 runnable 状态并重新添加到就绪队列中.
抢占调度
监控线程
在 go 程序运行时,会启动一个全局唯一的监控线程——sysmon thread,其负责定时执行监控工作,主要包括:
- 执行 netpoll 操作,唤醒 io 就绪的 g
- 执行 retake 操作,对运行时间过长的 g 执行抢占操作
- 执行 gcTrigger 操作,探测是否需要发起新的 gc 轮次
系统调用
系统调用是thread粒度的,在执行期间会导致整个m暂时不可用,此时,把发起syscall的 g和m绑定,但是解除p和m的绑定关系,使得此期间,p存在于其他m绑定的机会
发起系统调用时
- 将p和g的状态更新为 syscall
- 解除p和m的绑定
当系统调用结束后
- 检查之前的p 是否有和其他m绑定,如果没有,直接复用之前的p,继续执行g
- 通过mcall操作切换至g0执行,尝试为当前m寻找下一个g
- 若寻找成功,继续执行g
- 若寻找失败,则将g添加到global queue中,然后暂停m
运行超时
除了系统调用抢占之外,当 sysmon thread 发现某个 g 执行时间过长时,也会对其发起抢占操作

- 监控线程会执行retake 方法,检测哪些p中运行了一个g的单次运行时长超过了10ms,对其进行抢占操作
- 会对目标 g 所在的 m 发送抢占信号 sigPreempt,通过改写 g 程序计数器(pc,program counter)的方式将 g 逼停
对于IO事件
IO事件处理架构:
- 使用Linux的epoll机制
- 由独立的netpoller线程处理,
netpoller是系统级线程,不属于GMP模型
Goroutine状态转换:
- 当G遇到IO操作时,从running变为waiting,
和原来的M解绑
,进入netpoller的waiting队列等待
- IO就绪后,被重新放入调度队列
优势:
- 避免了每个M都执行epoll_wait
- 减少了系统调用开销
- 提高了IO处理效率
总结
GMP的核心优势
- 高效的内存使用
- g的栈空间可以动态伸缩
- 充分复用空闲资源
- 灵活的调度
- g可以动态绑定 P 和 M
- 用户态的调度
- 阻塞控制在g级别
- 避免内核调度的开销
ref
Prev
epoll
Next
openEBS lvm_localpv
Loading...