运行时
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,拿不到,则开启工作窃取
触发调度的条件
- 使用 go 关键字
- GC
- 系统调用
- 同步互斥操作
工作窃取
从其他 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 运行