Go-GMP调度

用户态线程

GMP

在1.2版本之前是没有p的概念的 只有 MG

Go 语言基于 GMP 模型实现用户态线程(协程)

  • G:表示 goroutine,每个 goroutine 都有自己的栈空间,定时器
    初始化的栈空间在 2k 左右,空间会随着需求增长。

  • M:表示 Machine,抽象化代表内核线程,记录内核线程栈信息
    当 goroutine 调度到线程时,使用该 goroutine 自己的栈信息。

  • P: processor 代表调度器,负责调度 goroutine
    维护一个本地 goroutine 队列,M 从 P 上获得 goroutine 并执行,同时还负责部分内存的管理。

在 Go 1.5 及之前的版本中,默认情况下,m 的最大数量为 GOMAXPROCS(默认为 CPU 核心数量),p 的最大数量为 256。在 Go 1.6 及之后的版本中,这些数量已经不再有硬编码的限制,而是根据运行环境动态调整,以获得更好的性能表现。

src/runtime/proc.go 中可以看到:

sched.maxmcount = 10000   // maximum number of m's allowed (or die)   m的最大数量

 

上图说明

sudog阻塞队列

每个阻塞操作都会创建一个对应类型的 sudog,并将其插入到对应类型的等待队列中进行等待。当该操作可以被执行时,就会从等待队列中取出对应的 sudog,并将其唤醒,使其重新进入可执行状态,以继续执行原来的代码。

sudog 实际上是一个通用的同步数据结构,Go 中有多种类型的 sudog,每种类型的 sudog 都对应着不同的同步操作。

sudog 主要有以下几种类型:

  1. send sudog:用于实现向 chan 发送数据的同步操作;
  2. recv sudog:用于实现从 chan 接收数据的同步操作;
  3. select sudog:用于实现 select 语句中的多路复用;
  4. timer sudog:用于实现定时器相关的同步操作;
  5. semacquire sudog:用于实现信号量的同步操作;
  6. mutex sudog:用于实现互斥锁的同步操作。

每种 sudog 都有自己独有的属性和方法,用于实现特定的同步操作。例如,send sudog 包含了发送的数据和目标 chan 的指针,recv sudog 则包含了接收数据的指针和目标 chan 的指针等。每个 sudog 都被插入到对应的等待队列中,等待被唤醒执行。

需要注意的是,虽然每种 sudog 都对应着不同的同步操作,但它们的底层实现都是相同的,都是通过锁和条件变量实现的。只是在不同的场景下,使用不同类型的 sudog 实现不同类型的同步操作而已。

gFree(全局自由 G 列表)

gFree 列表是一个全局的数据结构,被所有的 P(processor)所共享。当一个 P 需要创建一个新的 goroutine 时,它会首先尝试从本地的 gFree 列表中取出一个空闲的 G,如果本地列表为空,则会尝试从全局的 gFree 列表中获取。如果全局列表也为空,那么 P 就会根据需要创建一个新的 G。

总之,gFree 列表是 GMP 中的一个重要数据结构,用于存储和管理空闲的 G。它可以减少 G 的创建和销毁的开销,提高 goroutine 的创建和执行效率。

pidle(全局空闲P列表)

pidle 列表则是用于存储空闲的 P 的,当一个 P 不再需要执行任务时,它会将自己放入到 pidle 列表中,等待下次被复用。当需要一个新的 P 时,就可以从 pidle 列表中取出一个空闲的 P,如果pidle列表为空,则会尝试创建一个新的 P。

需要注意的是,pidle 列表是一个全局的数据结构,被所有的 P 所共享。当一个 P 需要创建或释放 goroutine 时,它会首先尝试从本地的 pidle 列表中获取或放入一个 P,如果本地列表为空,则会尝试从全局的 pidle 列表中获取或放入一个 P。这样可以避免在创建和销毁 P 的时候产生过多的开销,提高 goroutine 的执行效率。

G 所处的位置

  • 进程都有一个全局的 G 队列

  • 每个 P 拥有自己的本地执行队列

  • 有不在运行队列中的 G

    • 处于 channel 阻塞态的 G 被放在 sudog

    • 脱离 P 绑定在 M 上的 G,如系统调用

    • 为了复用,执行结束进入 P 的 gFree 列表中的 G

Goroutine 有哪几种状态?

  • _Gidle:刚刚被分配并且还没有被初始化

  • _Grunnable:没有执行代码,没有栈的所有权,存储在运行队列中

  • _Grunning:可以执行代码,拥有栈的所有权,被赋予了内核线程 M 和处理器 P

  • _Gsyscall:正在执行系统调用,拥有栈的所有权,没有执行用户代码,被赋予了内核线程 M 但是不在运行队列上

  • _Gwaiting:由于运行时而被阻塞,没有执行用户代码并且不在运行队列上,但是可能存在于 Channel 的等待队列上

  • _Gdead:没有被使用,没有执行代码,可能有分配的栈

  • _Gcopystack:栈正在被拷贝,没有执行代码,不在运行队列上

  • _Gpreempted:由于抢占而被阻塞,没有执行用户代码并且不在运行队列上,等待唤醒

  • _Gscan:GC 正在扫描栈空间,没有执行代码,可以与其他状态同时存在

Goroutine 创建过程

  • 获取或者创建新的 Goroutine 结构体

    • 从处理器的 gFree 列表中查找空闲的 Goroutine

    • 如果不存在空闲的 Goroutine,会通过 runtime.malg 创建一个栈大小足够的新结构体

  • 将函数传入的参数移到 Goroutine 的栈上

  • 更新 Goroutine 调度相关的属性,更新状态为_Grunnable

  • 返回的 Goroutine 会存储到全局变量 allgs 中

将 Goroutine 放到运行队列上

  • Goroutine 设置到处理器的 runnext 作为下一个处理器 执行的任务

  • 当处理器的本地运行队列已经没有剩余空间时(好像是256大小),就会把 本地队列中的一部分 Goroutine 和待加入的 Goroutine 通过 runtime.runqputslow 添加到调度器持有的全局 运行队列上

调度器行为

  • 为了保证公平,当全局运行队列中有待执行的 Goroutine 时,通过 schedtick 保证有一定 几率(1/61)会从全局的运行队列中查找对应的 Goroutine

  • 从处理器本地的运行队列中查找待执行的 Goroutine

  • 如果前两种方法都没有找到 Goroutine,会通过 runtime.findrunnable 进行阻塞地查找 Goroutine

    • 从本地运行队列、全局运行队列中查找

    • 从网络轮询器中查找是否有 Goroutine 等待运行

    • 通过 runtime.runqsteal 尝试从其他随机的处理器中窃取待运行的 Goroutine

 

posted @ 2023-04-20 15:06  GJH-  阅读(50)  评论(0编辑  收藏  举报