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.go
用 uintptr 代替指针,不被 gc 追踪,避免写屏障在坏时机发出
G 和 P 从不释放,M 会释放
g 含 *m,p 含 *m,m 含 *curg、*p
g和p不互相指向,要通过m沟通
g.sched 是 gobuf 类型,G 上下文切换时要保存的状态,如 sp (stack pointer)、pc (program counter) 等
sched 是 schedt 类型,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 被重用去运行另一个 tasknote是一次性通知,调用notewakeup后,后续notesleep都会立即返回- 要在
notesleep阻塞返回后noteclear,note才能再次使用
注:mutex 和 note 都是基于 futex 或 sema 实现
gopark 和 goready 直接与 scheduler 交互,不阻塞线程
gopark将 task 置于 waiting 态并从 scheduler 的运行队列移除,重用当前线程和 P 运行另一个 taskgoready将 task 置于 runnable 态并加入 scheduler 的运行队列
总结起来:
| Blocks | |||
|---|---|---|---|
| Interface | G | M | P |
| (rw)mutex | Y | Y | Y |
| note | Y | Y | Y/N |
| park | Y | N | N |
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,要拆成两个函数。