运行时

M 封装了系统线程,P 作为 G 的执行上下文,每个 P 会维护自己的一个队列,避免从全局队列拿 G ,降低锁的粒度

初始化

解析命令行参数,初始化一个 m0,启动一个系统线程与m0关联

栈空间初始化,创建内存分配器和垃圾回收协程

根据参数创建一定数量的 P,初始情况下与核数相同

第一个 goroutine 运行 runtime.Main(),启动 SYSMON 监控线程监控垃圾回收和协程调度管理

先执行 main 包中的 init 函数,函数的内容由编译器生成 main函数结束 exit(0) 退出

goroutine 创建

每个新建的 G 只有 4KB 的栈大小,1G的内存最多可以容纳 25万个 G

runtime.newproc()

  • 计算栈空间大小
  • 拿到当前 M 的 P 的指针,先获取 P 空闲链表中的 G 对象(复用发生在这!)
  • P没有空闲的就会去全局队列获取 G
  • 没有,再初始化一个 2KB 的函数栈(空闲的需要先清理;延迟计算均摊设计),还没扩容
  • 将 G 放置到空闲的 P 上,再与空闲的 M 关联起来,进入运行态 Grunning

goroutine 状态转换

本质上就是把 G 放置在不同的地方

发生系统调用的时候 - 调用 gopark() 将函数 G 阻塞,放在特定的阻塞队列中

恢复的时候 - 调用 goready() 从阻塞队列中唤醒,放回就绪队列

执行结束 - 取出当前 G 对象,修改状态为 Gdead ,使用 gfput将 G 挂入 P 的空闲队列中,如果有扩容,就将空间归还,只剩下初始函数栈

M

G 发生了一个系统调用,P 会被 SYSMON 解绑定,去寻找新的 M 执行 startM

M 也有一个全局的空闲队列(线程池),拿不到,再 newm()

为新创建的线程分配线程栈,使用系统调用创建内核级线程,放入 M

开始调度逻辑运行调度器,循环往复直到拿到 G,执行完后回收 G,如此循环往复

调度器

GC以及STW的检查,保证 STW 的时候不进行调度

获取 G,并执行 G

获取 G 的时候还会判断是否已经多次从 P 的本地队列中获取,如果是,那就获取全局锁去全局队列获取 G,拿不到,则开启工作窃取

触发调度的条件

  1. 使用 go 关键字
  2. GC
  3. 系统调用
  4. 同步互斥操作

工作窃取

从其他 M 的 P 上偷取一半的 G 放到自己队列,P 由随机算法进行确定,窃取 4 次

若失败,当前 M 会被 SYSMON 停止休眠,系统调用时间过长也会也会停止 M 的继续执行,取消自旋状态,将 M 放入闲置队列,将 M 的系统线程 park,与 P 解绑

栈空间扩容

首先从分配器的栈缓存 mcache 中拿,不足再从堆 mheap 中分配,还不足会向系统申请

新栈是老栈的两倍,校验是否大于栈分配最大值,只有 running 的 G 可以进行栈扩容 copystack

栈释放

shrinkstack 收缩的本质也是栈拷贝

系统调用

用户参数数据->寄存器<-内核执行结果 Go 对系统调用进行了包装,一般 syscall 会保存现场但不会让出 P ,如果系统调用时间长 SYSMON 会将 P 和 M 进行分离

系统调用时主动让出 P ,调用结束后尝试优先匹配原 P

SYSMON

这个监控线程可以在没有 P 的情况下直接在 M 运行