typora/note/Go/GMP.md
2024-12-12 10:48:55 +08:00

6.5 KiB
Raw Permalink Blame History

Go goroutine调度

  • 目的:将 Goroutine 按照一定算法放到不同的操作系统线程中去执行
  • 调度模型与算法几经演化,从最初的 G-M 模型、到 G-P-M 模型,从不支持抢占,到支持协作式抢占,再到支持基于信号的异步抢占

GM模型

  • 每个 Goroutine 对应于运行时中的一个抽象结构G(Goroutine)
  • 被视作“物理 CPU”的操作系统线程则被抽象为另外一个结构M(machine)

GM不足

  • 单一全局互斥锁(Sched.Lock) 和集中状态存储的存在,导致所有 Goroutine 相关操作,比如创建、重新调度等,都要上锁
  • Goroutine 传递问题M 经常在 M 之间传递“可运行”的 Goroutine这导致调度延迟增大也增加了额外的性能损耗
  • 每个 M 都做内存缓存,导致内存占用过高,数据局部性较差
  • 由于系统调用syscall而形成的频繁的工作线程阻塞和解除阻塞导致额外的性能损耗

集中状态(centralized state)就是一把全局锁要保护的数据太多。这样无论访问哪个数据都要锁这把全局锁。数据局部性差是因为每个m都会缓存它执行的代码或数据但是如果在m之间频繁传递goroutine那么这种局部缓存的意义就没有了。无法实现局部缓存带来的性能提升。

GMP模型

  • 在 Go 1.1 版本中实现了 G-P-M 调度模型和work stealing 算法,这个模型一直沿用至今

    • P 是一个“逻辑 Proccessor”每个 GGoroutine要想真正运行起来首先需要被分配一个 P也就是进入到 P 的本地运行队列local runq
    • 对于 G 来说P 就是运行它的“CPU”可以说在 G 的眼里只有 P
    • 从 Go 调度器的视角来看真正的“CPU”是 M只有将 P 和 M 绑定,才能让 P 的 runq 中的 G 真正运行起来
  • Go 1.1 模型不支持抢占式调度

  • GO 1.2 加入抢占式调度原理就是Go 编译器在每个函数或方法的入口处加上了一段额外的代码 (runtime.morestack_noctxt),让运行时有机会在这段代码中检查是否需要执行抢占调度,弊端就是只有在函数或方法不能能加入代码,纯算法循环代码无法加入调度功能(比如死循环)

  • Go 1.14 版本中增加了对非协作的抢占式调度的支持,这种抢占式调度是基于系统信号的,也就是通过向线程发送信号的方式来抢占正在运行的 Goroutine

协作式大家都按事先定义好的规则来比如一个goroutine执行完后退出让出p然后下一个goroutine被调度到p上运行。这样做的缺点就在于 是否让出p的决定权在groutine自身。一旦某个g不主动让出p或执行时间较长那么后面的goroutine只能等着没有方法让前者让出p导致延迟甚至饿死。而非协作: 就是由runtime来决定一个goroutine运行多长时间如果你不主动让出对不起我有手段可以抢占你把你踢出去让后面的goroutine进来运行

G

  • goroutine 缩写每次go func()都代表一个G无限制而且 G 对象是可以重用的
  • 使用struct runtime.g包含了当前goroutine的状态堆栈和上下文

M

  • 工作线程OS thread也被称为Machine使用 struct runtime.m所有M是有线程栈的1-8M
  • 执行流程是从 P 的本地运行队列以及全局队列中获取 G切换到 G 的执行栈上并执行 G 的函数,调用 goexit 做清理工作并回到 M如此反复
  • 如果不对该线程栈提供内存的话,系统会给该线程栈提供内存(不同操作系统提供的线程栈大小不同)。当指定了线程栈,则 M.stack→G.stackM 的 PC 寄存器指向 G 提供的函数,然后去执行

P

  • Processor是一个抽象概念并不是真正的物理CPU
  • 代表了M所需的上下文环境也是处理用户级代码逻辑的处理器。负责衔接M和G的调度上下文将等待执行的G和M进行连接。当P有任务是需要创建或者唤醒一个M来执行 它队列里的任务所以P/M需要进行绑定构成一个执行单元
  • P决定了并发任务的数量通过runtime.GOMAXPROCS来设定Go1.5之后被默认设置为可用核数之前默认设置为1

GMP优化部分

  • netpoller即便 G 发起网络 I/O 操作,也不会导致 M 被阻塞(仅阻塞 G也就不会导致大量线程M被创建出来
  • io poller这个功能可以像 netpoller 那样,在 G 操作那些支持监听pollable的文件描述符时仅会阻塞 G而不会阻塞 M。不支持对常规文件的监听

协作抢占式监控线程 sysmon M

  • M 的特殊之处在于它不需要绑定 P 就可以运行(以 g0 这个 G 的形式)
  • sysmon 每 20us~10ms 启动一次
  • 释放闲置超过 5 分钟的 span 内存
  • 如果超过 2 分钟没有垃圾回收,强制执行
  • 将长时间未处理的 netpoll 结果添加到任务队列
  • 向长时间运行的 G 任务发出抢占调度
  • 收回因 syscall 长时间阻塞的 P

sysmon M 抢占调度goroutine

  • 如果一个 G 任务运行 10mssysmon 就会认为它的运行时间太久而发出抢占式调度的请求
  • 一旦 G 的抢占标志位被设为 true等到这个 G 下一次调用函数或方法时,运行时就可以将 G 抢占并移出运行状态,放入队列中,等待下一次被调度

channel 阻塞或网络 I/O 情况下的调度

  • G 被阻塞在某个 channel 操作或网络 I/O 操作上时G 会被放置到某个等待wait队列中而 M 会尝试运行 P 的下一个可运行的 G
  • 如果这个时候 P 没有可运行的 G 供 M 运行,那么 M 将解绑 P并进入挂起状态
  • 当 I/O 操作完成或 channel 操作完成,在等待队列中的 G 会被唤醒标记为可运行runnable并被放入到某 P 的队列中,绑定一个 M 后继续执行

系统调用阻塞情况下的调度

  • G 被阻塞在某个系统调用system call不光 G 会阻塞,执行这个 G 的 M 也会解绑 P与 G 一起进入挂起状态
  • 如果此时有空闲的 M那么 P 就会和它绑定,并继续执行其他 G
  • 如果没有空闲的 M但仍然有其他 G 要去执行Go 运行时就会创建一个新 M线程
  • 当进行一些慢系统调用的时候比如常规文件io执行系统调用的m就要和g一起挂起这是os的要求不是go runtime的要求。毕竟真正执行代码的还是m