golang GMP调度模型

1、Golang调度器由来

  • 单进程时代的问题

    • 单一执行流程,计算机只能一个一个的处理任务
    • 进程阻塞带来CPU时间的浪费
  • 多进程、多线程的问题

    • 设计变得复杂
      • 进程/线程数量越多,切换成本就越大
      • 多线程随着同步竞争(锁,竞争资源冲突等)
  • 多线程/进程的壁垒

    • 高内存占用
      • 进程占用内存:虚拟内存4G(32bit OS)
    • 线程占用内存:约为4MB
    • 高CPU调度消耗
    • 协程(co-routine)引发的问题
      • N:1
        • 无法利用多个CPU
        • 出现阻塞的瓶颈
      • 1:1
        • 切换线程/进程代价昂贵
      • N:M
        • 能够利用多核
        • 过于依赖协程的优化和算法
  • 调度器的优化

    • Goroutine优化
      • 内存占用 几KB 可以大量开辟
      • 灵活调度 切换成本低
    • 早期GO调度器
      • 基本的全局GO队列和传统的轮询利用多个thread去调度
    • 弊端
      • 创建、销毁、调度G都需要每个M获取锁,形成激烈的锁竞争
      • M转移G会造成延迟和额外的系统负载
      • 系统调用导致频繁的线程阻塞和取消阻塞操作增加了系统的开销

2、Goroutine调度器GPM模型的设计思想

1、GMP模型简介

  • GMP
    • G:goroutine 协程
    • M:thread 线程
    • P:processor 处理器
  • 全局队列
    • 存放等待运行的G
  • P的本地队列
    • 存放等待运行的G
    • 数量限制 不超过256个G
    • 优先将新创建的G放在P本地队列,若满了就会放到全局队列
  • P列表
    • 程序启动时创建
    • 最多有GOMAXPROCS个(可配置)
  • M列表
    • 当前操作系统分配到当前GO程序的内核线程数
  • P和M的数量
    • P数量问题
      • 环境变量$GOMAXPROCS
      • 在程序中使用runtime.GOMAXPROCS()设置
    • M数量问题
      • GO语言本身限制M最大数量为10000
      • runtime/debug包中的setMaxThreads()设置
      • 一个M阻塞会创建一个新的M
      • 若有M空闲,就会回收或者睡眠

2、调度器的设计策略

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

3、"go func()"执行过程

  1. 通过go func()创建一个G
  2. 有两个存储G的队列,,一个是局部调度器的本地队列P,一个是全局G队列,新创建的G会先保存在本地队列P,若P满了就会保存到全局G队列
  3. G只能运行在M中,一个M必须持有一个P,M会从P的本地队列弹出一个可以执行的G执行,若P的本地队列为空,就会从全局G队列或其他MP组合偷取一个可执行的G来执行
  4. 一个M调度G执行的过程是一个循环机制
  5. 当M执行某一个G时若发生了,syscall或者其他阻塞操作,M会阻塞,若当前有一些G在执行,runtime就会把这个线程M从P中摘除(detach),然后创建一个新的线程(若有空闲线程则复用)服务于这个P
  6. 、当M系统调用结束时,这个G会尝试获取一个空闲的P执行,并放入到这个P的本地队列,若获取不到P,次线程M就会进入休眠状态,加入到空闲线程中,然后这个G会被放入全局队列

4、调度器生命周期

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

5、可视化GMP编程

  • 基本的trace编程
    • 创建trace文件
      • f, err := os.Create(“trace.out”)
    • 开启trace
      • trace.Start(f)
    • 关闭trace
      • teace.Stop(f)
    • go build且运行后会得到trace.out文件
  • 通过go tool trace工具打开trace文件
  • 通过debug trace查看GMP信息,GODEBUG=schedtrace=1000 ./可执行程序
    • SCHED 调试的信息
    • 0ms从程序启动到输出的时间
    • gomaxprocs P的数量 默认和核心数一致
    • idleprocs处理idle状态的P的数量 gomaxprocs-idleprocs=当前正在执行P的数量
    • threads 全部线程数
    • spinningthreads 处于自旋状态的线程
    • idlethread处于idle状态的thread
    • runqueue 全局G队列中G的数量
    • [0,0] 每个P的本地队列中,目前存在G的数量
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值