Go中的调度器:Goroutine状态转换与调度时机
字数 1793 2025-11-23 16:00:56
Go中的调度器:Goroutine状态转换与调度时机
在Go并发模型中,调度器负责管理Goroutine的生命周期和执行。理解Goroutine的状态转换和调度时机对于编写高效的并发程序至关重要。
1. Goroutine的三种基本状态
- Grunnable:可运行状态。Goroutine已创建并准备好执行,但尚未被分配到线程
- Grunning:运行状态。Goroutine正在某个线程上执行代码
- Gsyscall:系统调用状态。Goroutine正在执行阻塞的系统调用
- Gwaiting:等待状态。Goroutine因某些条件(如channel操作、锁、定时器等)而阻塞
2. 状态转换的详细过程
从创建到可运行(→ Grunnable)
go func() {
fmt.Println("New goroutine")
}()
- 步骤1:编译器将go语句转换为运行时调用
runtime.newproc() - 步骤2:分配Goroutine结构体(g结构)和初始栈空间(通常2KB)
- 步骤3:将函数参数复制到Goroutine的栈中
- 步骤4:将Goroutine加入当前P的本地运行队列(local runqueue)
从可运行到运行(Grunnable → Grunning)
- 触发条件:调度器寻找可执行的Goroutine
- 步骤1:当前运行的Goroutine主动让出或阻塞
- 步骤2:调度器从当前P的本地队列获取下一个Grunnable的G
- 步骤3:执行
runtime.execute()将G绑定到当前M(线程) - 步骤4:修改G状态为Grunning,恢复G的上下文并跳转到代码位置
从运行到等待(Grunning → Gwaiting)
ch := make(chan int)
// 当前Goroutine在此处阻塞
data := <-ch // 或 ch <- 1
- 步骤1:Goroutine执行阻塞操作(channel发送/接收、锁获取等)
- 步骤2:调用相应的运行时函数(如
runtime.chansend()) - 步骤3:检查操作是否可立即完成,若不能则修改状态为Gwaiting
- 步骤4:将G从当前M解绑,并加入相应等待队列
- 步骤5:触发调度,寻找下一个可运行的G
从等待到可运行(Gwaiting → Grunnable)
// 在另一个Goroutine中
ch <- 42 // 唤醒等待的Goroutine
- 步骤1:其他Goroutine完成使条件满足的操作(如channel发送)
- 步骤2:运行时将等待的G从等待队列移至可运行队列
- 步骤3:根据情况选择目标P的队列:
- 如果G有绑定的P,加入其本地队列
- 否则加入全局队列或窃取者的队列
- 步骤4:修改G状态为Grunnable,等待被调度
系统调用相关的状态转换
file, err := os.Open("test.txt") // 可能触发系统调用
-
进入系统调用(Grunning → Gsyscall):
- 步骤1:G执行阻塞系统调用前调用
runtime.entersyscall() - 步骤2:解除G与当前P的绑定(P进入Psyscall状态)
- 步骤3:P可能被分配给其他M继续执行其他G
- 步骤1:G执行阻塞系统调用前调用
-
退出系统调用(Gsyscall → Grunnable):
- 步骤1:系统调用返回时调用
runtime.exitsyscall() - 步骤2:尝试获取原来的P,如果可用则直接绑定
- 步骤3:如果原P已被占用,尝试从其他P窃取工作
- 步骤4:如果都失败,则G加入全局队列,M进入休眠
- 步骤1:系统调用返回时调用
3. 主要的调度时机
主动调度(主动让出CPU)
// 1. 调用runtime.Gosched()
func main() {
go func() {
for {
runtime.Gosched() // 主动让出CPU
}
}()
}
// 2. 在函数入口插入的栈扩张检查
// 编译器会在函数开头插入栈检查代码
被动调度(因阻塞操作触发)
- Channel操作:发送/接收阻塞时
- 网络I/O:网络读写未就绪时
- 系统调用:文件操作、sleep等
- 同步原语:mutex、cond等锁操作
抢占式调度(防止Goroutine独占CPU)
- 基于信号的抢占(主要机制):
- 步骤1:监控线程sysmon检测到G运行超过10ms
- 步骤2:向对应M发送SIGURG信号
- 步骤3:信号处理程序修改G的上下文,使其在下次检查点时让出
- 基于函数调用的协作式抢占(Go 1.14前):
- 在函数调用时检查抢占标志
4. 调度器的负载均衡机制
工作窃取(Work Stealing)
- 当P的本地队列为空时,按顺序尝试:
- 从全局队列获取一批G(最多1/4的本地队列容量)
- 从网络轮询器获取已就绪的G
- 随机选择其他P,窃取其本地队列一半的G
系统调用返回时的P获取策略
- 优先尝试获取原来的P
- 次优尝试从其他空闲P窃取
- 最后将G加入全局队列,M休眠
5. 实际调试与观察
查看调度器状态:
import "runtime"
import "runtime/debug"
// 查看Goroutine数量
println(runtime.NumGoroutine())
// 查看调度器跟踪信息
debug.SetTraceback("system")
// 使用trace工具
// go run main.go 2> trace.out
// go tool trace trace.out
理解这些状态转换和调度时机有助于:
- 诊断Goroutine泄漏问题
- 优化并发程序性能
- 理解死锁和活锁的产生原因
- 合理设计并发控制策略