goroutine调度器的由来

单进程的问题

  1. 单一执行流程, 计算只能一任务一个任务的处理
  2. 进程阻塞带来CPU时间的浪费

    多进程、多线程的问题

  3. 设计变得复杂
    进程、线程的数量越多,切换成本越大,也就也浪费 多线程追着同步竞争(如 锁、竞争资源冲突等)
  4. 多进程、多线程的壁垒 高内存占用及高CPU切换

协程 co-routine的问题

  1. N:1
  • 无法利用多个CPU
  • 出现阻塞瓶颈
  1. 1:1
  • 与多线程无异
  1. M:N
  • 可以利用多核
  • 过度依赖协程调度器的优化和算法
  1. 调度器的优化

Goroutine的优化

  • 内存占用小 几kb
  • 灵活调度 切换成本低


    早期的GO的调度器
    基本的全局GO队列和比较传统的轮训利用多个thread去调度
    弊端

  1. 创建、销毁、调度G都需要每个M获取锁,造成激烈的锁竞争
  2. M转移G会造成延迟和额外的系统负载
  3. 系统调用(CPU在M之间的切换)导致频繁的线程阻塞和取消阻塞操作增加了系统开销

GMP模型介绍

1.png

GMP

  • G goroutine 协程
  • P processor 处理器
  • M thread 内核线程

全局队列
存放等待运行的G
P的本地队列

  • 存放等待运行的G
  • 数量限制:不超过256
  • 优先将G放入本地队列,当本地队列满足了之后,才会放入全局队列

P列表

  • 程序启动时创建
  • 最多有GOMAXPROCS个

M列表
当前操作系统分配到当前GO程序的内核线程数
P和M的数量

  1. P的数量问题
  • 环境变量GOMAXPROCS设置
  • runtime.GOMAXPROCS()设置
  1. M的数量
  • GO最大M限制是10000
  • runtime/debug包中的setMaxThrads函数来限制
  • 有一个M阻塞,会创建一个新的M
  • 如果M有空闲,就会回收或者睡眠

调度器的设计思想

复用线程
避免频繁的创建和销毁线程,而是对线程的复用
work stealing机制
当本线程无可运行的G时,尝试从其他线程绑定的P偷取G,而不是销毁线程(优先去全局P)
head off机制
当本地线程因为G进行系统调用阻塞时,线程释放绑定的P,把P转移给其他空闲的线程执行
利用并行
GOMAXPROCS设置P的数量,最多有GOMAXPROCS个线程分布在多个CPU上运行
抢占
在coroutine中要等待一个协程主动让出CPU才执行下一个协程,在GO中,一个goroutine最多占用10ms,防止goroutine被饿死
全局队列
当M执行work stealing从其他P偷不到时,它可以从全局G队列获取G

go func(){} 经历什么过程

1.png
1. go func(){} 会创建一个goroutine

  1. 新建的goroutine会优先加入P的本地队列中,如果P的本地队列已经满了就会保存到全局队列中。

  2. G只能运行在M中,一个M必须持有一个P,M会从本地的队列弹出一个可执行状态的G来执行。

  3. 一个M调度G的执行过程是一个循环

  4. 当M执行一个G的发生syscall或者其余阻塞操作,M会阻塞,如果当前有一些G在执行,runtime会把这个线程M从P中摘除,然后创建一个新的操作系统的线程来服务这个P

  5. 当M系统调用结束时,这个G会进入其他的P或者全局队列,M会尝试获取P,如果获取不到,会变成休眠状态

调度器的生命周期

M0
M0是启动程序后编号为0的主线程,这个M对应的实例会在全局便连runtime.m0中,不需要在heep上分配,M0负责执行初始化操作和启动第一个G,在之后M0就和其他M一样了
G0
G0是每次启动一个M0都会第一个创建的goroutine,G0仅用于负责调度的G,G0不指向任何可执行的函数,每个M都会有一个自己的G0.。在调度或系统调用时会使用G0的栈空间,全局变量的G0是M0的G0

GMP调试

go tool trace

  1. func main() {
  2. f, err := os.Create("trace.out")
  3. if err != nil {
  4. panic(err)
  5. }
  6. defer f.Close()
  7. err = trace.Start(f)
  8. if err != nil {
  9. panic(err)
  10. }
  11. fmt.Println("GMP")
  12. trace.Stop()
  13. }

DEBUG trace

GODEBUG=schedtrace=1000 ./可执行程序
2.png

n ms
从程序运行到输出经历的时间
gomaxprocs
P的数量, 一般默认和CPU的核心数一致
idleprocs
处理idle状态的P的数量,gomaxprocs-idleprocs=目前正在执行的P的数量
threads
线程数量(包括M0,包括GODEBUG调试的线程)
spinningtreads
处于自旋状态的thread数量
idlethread
处理idle状态的thread
image.png
renqueue
全局G队列中的G的数量
[0,0]
每个P的local queue本地队列中,目前存在的G的数量

场景分析

G2会优先加入G1所在的本地队列
1.png2.png3.png4.png5.png6.png
公式: n = min(len(GQ)/GOMAXPROCS+1, len(GQ)/2)7.png8.png9.png10.png11.png