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)状态

  1. 如果一个工作线程的本地队列、全局运行队列或网络轮询器中均没有可调度的任务,则该线程成为自旋线程;
  2. 满足该条件、被复始的线程也被称为自旋线程,对于这种线程,运行时不做任何事情。

自旋线程在进行暂止之前,会尝试从任务队列中寻找任务。当发现任务时,则会切换成非自旋状态, 开始执行 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 通过指针互相关联。

 

posted @ 2020-05-27 22:13  是的哟  阅读(445)  评论(0编辑  收藏  举报