Goroutine调度方式
Contents
关于GMP模型
- 就很符合人类思考的一中设计吧;G负责发起,P负责安排,M负责执行
- 1.1版本才有的P,使M可以专注于执行
- 再加点工作窃取之类的技巧,压榨下cpu
- work stealing 机制:当本线程无可运行的 G 时,尝试从其他线程绑定的 P 偷取 G,而不是销毁线程
- hand off 机制:当本线程因为 G 进行系统调用阻塞时,线程释放绑定的 P,把 P 转移给其他空闲的线程执行
- Go 调度本质是把大量的 goroutine 分配到少量线程上去执行,并利用多核并行,实现更强大的并发
调度方式
协作式
主动调度弃权:抢占标记
- 在函数调用的序言插入抢占检测指令,若检测到标记,则主动中断执行,让出执行权利
可以被抢占的条件
- 运行时没有禁止抢占(m.locks == 0)
- 运行时没有在执行内存分配(m.mallocing == 0)
- 运行时没有关闭抢占机制(m.preemptoff == “")
- M 与 P 绑定且没有进入系统调用(p.status == _Prunning)
stackguard0
设置抢占标记stackPreempt
的时机
Note
- 进入系统调用时(runtime.reentersyscall,注意这种情况是为了保证不会发生栈分裂, 真正的抢占是异步的通过系统监控进行的)
- 任何运行时不再持有锁的时候(m.locks == 0)
- 当垃圾回收器需要停止所有用户 Goroutine 时
- 存在问题:若协程中无函数,则无法设置抢占标记,也就无法被抢占
|
|
主动用户让权:Gosched
- 使用示例
|
|
|
|
- 如果去掉
runtime.GOMAXPROCS(1)
或者runtime.Gosched()
则打印不出来全部输出
Note
- 若有多个P,则go出来的协程会挂到别的P上
- 当只有一个P时,会通过循环中的
runtime.Gosched()
轮流让出 执行机会
抢占式
异步系统调用
- 比如一个协程进行网络IO操作,会被当前的MP抛给netpoller
- 在完成IO操作后,等待队列中的 G 会被唤醒,标记为可运行(runnable),并被放入到某 P 的队列中(抢P),绑定一个 M 后继续执行
同步系统调用
- 比如文件 I/O,MG 会和P分离(P另寻M,抢M)
- 当M从系统调用返回时,不会继续执行,而是将G放到run queue
被动 GC 抢占
- 当需要进行垃圾回收时,为了保证不具备主动抢占处理的函数执行时间过长,导致垃圾回收迟迟不得执行而导致的高延迟,而强制停止 G 并转为执行垃圾回收
抢占信号的选取
- 有关信号
- preemptM 完成了信号的发送,直接向需要进行抢占的 M 发送 SIGURG 信号 即可
- 为什么是 SIGURG 信号而不是其他的信号?如何才能保证该信号 不与用户态产生的信号产生冲突?是因为:
Note
- 默认情况下,SIGURG 已经用于调试器传递信号。
- SIGURG 可以不加选择地虚假发生的信号。例如,我们不能选择 SIGALRM,因为 信号处理程序无法分辨它是否是由实际过程引起的(可以说这意味着信号已损坏)。 而常见的用户自定义信号 SIGUSR1 和 SIGUSR2 也不够好,因为用户态代码可能会将其进行使用
- 需要处理没有实时信号的平台(例如 macOS)
- 考虑以上的观点,SIGURG 其实是一个很好的、满足所有这些条件、且极不可能因被用户态代码 进行使用的一种信号。
两个特殊的M
- 一个名为 NetPoller 的 M 异步处理网络IO,它不需要和 P 进行绑定。当 G 执行网络 IO 的时候,G 会将当前 M 和 P 解绑,进入到 NetPoller 的 M 中,等待网络 IO 完成,这样即使执行网络 IO 的系统调用,也不会产生阻塞的 M.当网络 IO 完成后,M 的 Schedule 函数,会通过 findrunable函数 取到这个 G,继续运行它
- 一个名为 sysmon 的 M的系统监控线程,它也不需要绑定 P 就可以运行(以 g0 这个 G 的形式),它的主要功能有:
Note
- 检查死锁runtime.checkdead
- 运行计时器 — 获取下一个需要被触发的计时器;
- 将长时间未处理的 netpoll 结果添加到任务队列;
- 向长时间运行的 G 任务发出抢占调度(retake 方法);Go 的抢占式调度当 sysmon 发现 M 已运行同一个 G(Goroutine)10ms 以上时,它会将该 G 的内部参数 preempt 设置为 true。然后,在函数序言中,当 G 进行函数调用时,G 会检查自己的 preempt 标志,如果它为 true,则它将自己与 M 分离并推入“全局队列”。由于它的工作方式(函数调用触发),在 for{} 的情况下并不会发生抢占,如果没有函数调用,即使设置了抢占标志,也不会进行该标志的检查。Go1.14 引入抢占式调度(使用信号的异步抢占机制),sysmon 仍然会检测到运行了 10ms 以上的 G(goroutine)。然后,sysmon 向运行 G 的 P 发送信号(SIGURG)。Go 的信号处理程序会调用P上的一个叫作 gsignal 的 goroutine 来处理该信号,将其映射到 M 而不是 G,并使其检查该信号。gsignal 看到抢占信号,停止正在运行的 G。
- 打印调度信息,归还内存等定时任务.
- 释放闲置超过 5 分钟的 span 内存;如果超过 2 分钟没有垃圾回收,强制执行;
- 收回因 syscall 长时间阻塞的 P