开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第14天,点击查看活动详情
定义:抢占式调度特质被抢占者被动中断而非主动让权
就像操作系统要负责线程的调度一样,Go的runtime要负责goroutine的调度。现代操作系统调度线程都是抢占式的,我们不能依赖用户代码主动让出CPU,或者因为IO、锁等待而让出,这样会造成调度的不公平。基于经典的时间片算法,当线程的时间片用完之后,会被时钟中断给打断,调度器会将当前线程的执行上下文进行保存,然后恢复下一个线程的上下文,分配新的时间片令其开始执行。这种抢占对于线程本身是无感知的,系统底层支持,不需要开发人员特殊处理。基于时间片的抢占式调度有个明显的优点,能够避免CPU资源持续被少数线程占用,从而使其他线程长时间处于饥饿状态。
因为整个Go程序都是运行在用户态的,所以不能像操作系统那样利用时钟中断来打断运行中的goroutine。也得益于完全在用户态实现,goroutine的调度切换更加轻量。goroutine的调度器也用到了时间片算法,但是和操作系统的线程调度还是有些区别的,主要分为基于协作的抢占式调度和基于信号量的抢占式调度。
- 协作式调度的核心在于G在M上运行的时候主动让出M
- 基于信号量的抢占式调度的本质是G没有主动让出M的时候,强行中断M对G的执行
1. 基于协作的抢占式调度
- 通过设置环境变量 GODEBUG=asyncpreempt=off启用
- 对于Go语言中运行时间过长的goroutine,会有一个后台线程持续监控,一旦运行超过10ms,会设置goroutine的协作标识位,资源会被抢占走。
- 基于协作的抢占式调度是基于编译器插入函数实现的,所以只有在函数调用的序言部分会设置“协作标志”,才能执行协作式调度。
2. 基于信号量的抢占式调度
-
目前基于信号量的抢占式调度只会在垃圾收集扫描任务时触发。垃圾收集需要暂停整个程序(STW),最长可能需要几分钟的时间,导致整个程序无法工作。抢占式调度用于接触长时间占用线程的问题。
-
系统监控中,preemptone会在支持M抢占的情况下发起preemptM的抢占信号。preemptM直接想需要进行抢占的M发送SIGURG信号。
- SIGURG:支持多平台;可以随意出现并没有任何触发后果;不会被内部的libc库使用拦截;需要被调试器使用