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
  • 退出系统调用(Gsyscall → Grunnable)

    • 步骤1:系统调用返回时调用runtime.exitsyscall()
    • 步骤2:尝试获取原来的P,如果可用则直接绑定
    • 步骤3:如果原P已被占用,尝试从其他P窃取工作
    • 步骤4:如果都失败,则G加入全局队列,M进入休眠

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的本地队列为空时,按顺序尝试:
    1. 从全局队列获取一批G(最多1/4的本地队列容量)
    2. 从网络轮询器获取已就绪的G
    3. 随机选择其他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泄漏问题
  • 优化并发程序性能
  • 理解死锁和活锁的产生原因
  • 合理设计并发控制策略
Go中的调度器:Goroutine状态转换与调度时机 在Go并发模型中,调度器负责管理Goroutine的生命周期和执行。理解Goroutine的状态转换和调度时机对于编写高效的并发程序至关重要。 1. Goroutine的三种基本状态 Grunnable :可运行状态。Goroutine已创建并准备好执行,但尚未被分配到线程 Grunning :运行状态。Goroutine正在某个线程上执行代码 Gsyscall :系统调用状态。Goroutine正在执行阻塞的系统调用 Gwaiting :等待状态。Goroutine因某些条件(如channel操作、锁、定时器等)而阻塞 2. 状态转换的详细过程 从创建到可运行(→ Grunnable) 步骤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) 步骤1:Goroutine执行阻塞操作(channel发送/接收、锁获取等) 步骤2:调用相应的运行时函数(如 runtime.chansend() ) 步骤3:检查操作是否可立即完成,若不能则修改状态为Gwaiting 步骤4:将G从当前M解绑,并加入相应等待队列 步骤5:触发调度,寻找下一个可运行的G 从等待到可运行(Gwaiting → Grunnable) 步骤1:其他Goroutine完成使条件满足的操作(如channel发送) 步骤2:运行时将等待的G从等待队列移至可运行队列 步骤3:根据情况选择目标P的队列: 如果G有绑定的P,加入其本地队列 否则加入全局队列或窃取者的队列 步骤4:修改G状态为Grunnable,等待被调度 系统调用相关的状态转换 进入系统调用(Grunning → Gsyscall) : 步骤1:G执行阻塞系统调用前调用 runtime.entersyscall() 步骤2:解除G与当前P的绑定(P进入Psyscall状态) 步骤3:P可能被分配给其他M继续执行其他G 退出系统调用(Gsyscall → Grunnable) : 步骤1:系统调用返回时调用 runtime.exitsyscall() 步骤2:尝试获取原来的P,如果可用则直接绑定 步骤3:如果原P已被占用,尝试从其他P窃取工作 步骤4:如果都失败,则G加入全局队列,M进入休眠 3. 主要的调度时机 主动调度(主动让出CPU) 被动调度(因阻塞操作触发) 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. 实际调试与观察 查看调度器状态: 理解这些状态转换和调度时机有助于: 诊断Goroutine泄漏问题 优化并发程序性能 理解死锁和活锁的产生原因 合理设计并发控制策略