🎗️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的架构

notion image
GMP模型是一个任务调度系统m是这个调度系统中的引擎,当m与p结合时,m不断交替执行g0和g

g

  • 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的运行,避免产生资源浪费
 

让渡的设计

notion image

结束让渡

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

主动让渡

notion image
主动让渡是指用户手动调用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 执行时间过长时,也会对其发起抢占操作
notion image
  • 监控线程会执行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的核心优势
  1. 高效的内存使用
    1. g的栈空间可以动态伸缩
    2. 充分复用空闲资源
  1. 灵活的调度
    1. g可以动态绑定 P 和 M
  1. 用户态的调度
    1. 阻塞控制在g级别
    2. 避免内核调度的开销
 

ref

Prev
epoll
Next
openEBS lvm_localpv
Loading...
Article List