scheduler 职责是匹配 G(要执行的代码)、M(执行代码的地方)和 P(执行代码所需的资源)

  • G: goroutine,要执行的 go 代码,即 task
  • M: machine,执行代码的地方,即 os 线程
  • P: processor,执行代码所需的资源(如 CPU核、runnable task 的队列)

P 初始有GOMAXPROCS个,当线程停止执行 task(如进入 syscall),P 回到空闲池;当线程恢复执行 task(如从 syscall 返回),要从空闲池获取 P。

如果线程M阻塞在syscall,可以起更多的M。

当 task 新建或 runnable 时,放入当前 P的队列中。当 P 执行完 task,从自己队列取 task;若队列为空,随机另选一个 P,偷走一半 tasks;若随机 P 的队列仍为空,继续随机另选一个 P。

随机遍历数组

随机另选 P 窃取 tasks 时,要随机遍历数组。对于数组[n]T,从 1..=n 与 n 互质(gcd(i,n)==1)的数组 []coprimes 中随机选一个素数 p。先从0..n中随机选一个下标 i,然后多次调用 i=(i+p)%n 就能随机遍历数组。

实现 randomOrder 中只调用一次 fastrand()生成随机数 r。用 r 的低位选 i(i = r%n),用 r 的高位选 p(p = coprimes[r/n%len(coprimes)])。

优先利用 spinning 线程

spinning 线程:线程运行完 task,若 P 队列、全局队列、netpoller 都没新 task,则进入 spinning 态。spinning 线程在 parking 前,会尝试从 per-P 队列、timer 堆 、gc(标记任务)寻找 task。

当存在 spinning 线程,不创建新线程。 当最后的 spinning 线程找到 task 并停止 spinning,新建一个 spinning 线程。

proc.go

  • startm(p) 取空闲线程或新建线程
  • wakep() 不存在 spinning 线程时,取一个空闲 P,启动一个 spinning 线程(startm(p, true)
  • releasep() 将 running 态的 P 和线程分离,将 P 设为 idle 态
  • handoffp(p) 若有 runnable task(如 per-P 或全局队列非空、gc 有标记任务等),复用 P 来 startm(p)
  • handoffp(releasep())
  • stoplockedm() 若 P 正在运行,则 P 跟线程分离并复用(handoffp(releasep())),park 线程(调用 notesleep,task 和线程一起阻塞),从阻塞返回后 requirep()
  • acquirep() 关联 p 和当前线程
  • acquirem() 禁止抢占
  • sched.lastpoll == 0 说明 scheduler 还在初始化阶段

结构

G/M/P 结构定义在 runtime/runtime2.gouintptr 代替指针,不被 gc 追踪,避免写屏障在坏时机发出 G 和 P 从不释放,M 会释放

g*mp*mm*curg*p gp不互相指向,要通过m沟通

g.schedgobuf 类型,G 上下文切换时要保存的状态,如 sp (stack pointer)、pc (program counter) 等 schedschedt 类型,scheduler

sudog 是等待队列中的g包装。g 和等待队列是多对多关系 sudog 有说当作 pseudo G,我猜可能是 SUspended Descriptor Of G 的缩写

pMask[]uint32 类型,atomic 位图,每个 P 占一位

通过 g.schedlink 把 Gs 链起来,gQueue 是有头尾指针的双端队列,gList 是只有头指针的链表

参考


运行时栈

getg() == getg().m.curg 表示当前 g 在用户栈运行

  • 若在系统栈运行,则 getg() == getg().M.g0
  • 若在信号栈运行,则 getg() == getg().m.gsignal

用户栈一开始很小(2kB),然后动态地增长或收缩 //go:nosplit 指令:用户栈不可增长(比如会导致死锁)

系统栈和信号栈不可增长 在系统栈运行的代码不可抢占、不会被 gc 扫描

同步

mutex 会阻塞线程,不与 scheduler 交互(阻止 task 和 P 被重新调度)

note 会阻塞线程

  • notesleep 就像 mutex,会阻止 task 和 P 被重新调度
  • notetsleepg  就像 syscall,允许 P 被重用去运行另一个 task
  • note 是一次性通知,调用 notewakeup 后,后续 notesleep 都会立即返回
  • 要在 notesleep 阻塞返回后 noteclearnote 才能再次使用

注:mutex 和 note 都是基于 futex 或 sema 实现

goparkgoready 直接与 scheduler 交互,不阻塞线程

  • gopark 将 task 置于 waiting 态并从 scheduler 的运行队列移除,重用当前线程和 P 运行另一个 task
  • goready 将 task 置于 runnable 态并加入 scheduler 的运行队列

总结起来:

Blocks
InterfaceGMP
(rw)mutexYYY
noteYYY/N
parkYNN

atomic

混合 atomic 和 non-atomic 访问的常见模式

  • 大部分读操作、写操作被锁保护的变量:在锁保护的范围内,读操作可以 non-atomic,但是写操作必须 atomic(写操作被锁和 atomic 两层保护)。在锁保护的范围外,读操作必须 atomic。
  • 在 STW 期间只有读操作、没有写操作的变量:读操作可以 non-atomic。

指令

写屏障需要有活跃的 P(getg().m.p != nil)。释放 P 后用 //go:nowritebarrierrec,获取 P 后用//go:yeswritebarrierrec。指令只能用一个在函数前,若函数内同时有释放 P 和获取 P,要拆成两个函数。

参考