golang底层 调度、其他
锁定g和m
在执⾏ cgo 调⽤时,会⽤ lockOSThread 将 G 锁定在当前线程
锁定操作很简单,只需设置 G.lockedm 和 M.lockedg 即可
lockedm 会休眠,直到某⼈将 lockedg 交给它。⽽不幸拿到 lockedg 的 M,
则要将 lockedg 连同 P ⼀起传递给 lockedm,还负责将其唤醒。⾄于⾃⼰,则因失去 P 被迫休眠,直到 wakep 带着新的 P 唤醒它
UnlockOSThread可主动解除锁定
系统调用
有两类系统调用:Syscall 和 RawSyscall
RawSyscall对应的系统调用都是非阻塞的,不需要runtime的参与,会立即返回,如果用RawSyscall去调用阻塞的系统调用,p将被浪费。
Syscall 增加了 entersyscall/exitsyscall,确保sysmon正常执行,sysmon会在系统调用长时间阻塞时,调度其他任务。有的系统调用一定会长时间阻塞,这些系统调用会执行entersyscallblock主动交出关联的p。退出系统调用的快速路径 exitsyscallfast 是指能重新绑定原有或空闲的 P,以继续当前 G 任务执⾏。如果多次尝试绑定 P 失败,那么只能走慢速路径,将当前任务放⼊待运⾏队列。
entersyscall -> reentersyscall -> entersyscall_sysmon -> notewakeup
entersyscallblock -> entersyscallblock_handoff -> handoffp(releasep)
exitsyscall -> exitsyscallfast -> exitsyscallfast_pidle
-> exitsyscall0
目前的 Go 的调度器实现中设计了工作线程的自旋(spinning)状态:
- 如果一个工作线程的本地队列、全局运行队列或网络轮询器中均没有可调度的任务,则该线程成为自旋线程;
- 满足该条件、被复始的线程也被称为自旋线程,对于这种线程,运行时不做任何事情。
自旋线程在进行暂止之前,会尝试从任务队列中寻找任务。当发现任务时,则会切换成非自旋状态, 开始执行 goroutine。而找到不到任务时,则进行暂止。
当一个 goroutine 准备就绪时,会首先检查自旋线程的数量,而不是去复始一个新的线程。
如果最后一个自旋线程发现工作并且停止自旋时,则复始一个新的自旋线程。 这个方法消除了不合理的线程复始峰值,且同时保证最终的最大 CPU 并行度利用率。
总的来说,调度器的方式可以概括为: 如果存在一个空闲的 P 并且没有自旋状态的工作线程 M,则当就绪一个 G 时,就复始一个额外的线程 M。 这个方法消除了不合理的线程复始峰值,且同时保证最终的最大 CPU 并行度利用率。
因此,就绪一个 G 的通用流程为:
- 提交一个 G 到 per-P 的本地工作队列
- 执行 StoreLoad 风格的写屏障
- 检查 sched.nmspinning 数量
而从自旋到非自旋转换的一般流程为:
- 减少 nmspinning 的数量
- 执行 StoreLoad 风格的写屏障
- 在所有 per-P 本地任务队列检查新的工作
M 的结构
- 持有用于执行调度器的 g0
- 持有用于信号处理的 gsignal
- 持有线程本地存储 tls
- 持有当前正在运行的 curg
- 持有运行 goroutine 时需要的本地资源 p
- 表示自身的自旋和非自旋状态 spining
- 管理在它身上执行的 cgo 调用
- 将自己与其他的 M 进行串联
- 持有当前线程上进行内存分配的本地缓存 mcache
调度器 sched 结构
- 管理了能够将 G 和 M 进行绑定的 M 队列
- 管理了空闲的 P 链表(队列)
- 管理了 G 的全局队列
- 管理了可被复用的 G 的全局缓存
- 管理了 defer 池
P,Processor,人为抽象的资源,数量通常等于cpu核数
P 只是处理器的抽象,而非处理器本身,它存在的意义在于实现工作窃取(work stealing)算法。 简单来说,每个 P 持有一个 G 的本地队列。
在没有 P 的情况下,所有的 G 只能放在一个全局的队列中。 当 M 执行完 G 而没有 G 可执行时,必须将队列锁住从而取值。
当引入了 P 之后,P 持有 G 的本地队列,而持有 P 的 M 执行完 G 后在 P 本地队列中没有 发现其他 G 可以执行时,虽然仍然会先检查全局队列、网络,但这时增加了一个从其他 P 的 队列偷取(steal)一个 G 来执行的过程。优先级为本地 > 全局 > 网络 > 偷取。
保存 g 的运行入口 gostartcallfn 将要执行的函数 fv 或者称 fn 保存到了 newg.sched 这个 buf 中
默认最多能创建10000个m,可用runtime/debug.SetMaxThreads修改这个值
⽤户可调⽤ runtime.Goexit ⽴即终⽌ G 任务,不管当前处于调⽤堆栈的哪个层次,G会被设为_Gdead状态并放到P的本地gfree列表中。在终⽌前,它确保所有 G.defer 被执⾏。在 main goroutine ⾥执⾏ Goexit,它会等待其他 goroutine 结束后才会崩溃。
所有 init 函数都在同⼀个 goroutine 内执⾏。
所有 init 函数结束后才会执⾏ main.main 函数。
m0 和 g0
每个m都有一个g0,运行在系统栈上,也叫线程栈或调度栈,g0不是go语句生成的,而是在创建m的时候由runtime生成的,一般用于执行调度、gc、内存管理等,g0不会阻塞,也不存在于任何列表中,栈不会被扫描。
m还有一个gsignal,用于处理信号,系统栈和信号栈不会自动增长
g0 和 gsignal 称为系统g,go语句创建的g称为用户g。
m0 和 m0 的 g0 是静态分配的,它们也叫做runtime.m0 和 runtime.g0,m0 是第一个系统线程,g0在此线程里执行引导程序, m0 与 g0 通过指针互相关联。