Goroutine调度 #

一、调度器概述 #

Go调度器负责将Goroutine分配到CPU上执行。

二、GMP模型 #

2.1 G (Goroutine) #

  • 协程,用户态线程
  • 包含栈、指令指针等信息
  • 初始栈2KB,可增长

2.2 M (Machine) #

  • 操作系统线程
  • 执行G的载体
  • 数量默认最多10000

2.3 P (Processor) #

  • 逻辑处理器
  • 包含运行队列
  • 默认数量等于CPU核心数

2.4 关系图 #

text
    全局队列
        │
        ▼
┌───┬───┬───┬───┐
│ P │ P │ P │ P │  ← 处理器
└─┬─┴─┬─┴─┬─┴─┬─┘
  │   │   │   │
  ▼   ▼   ▼   ▼
┌─┴─┬─┴─┬─┴─┬─┴─┐
│ M │ M │ M │ M │  ← 系统线程
└───┴───┴───┴───┘

三、调度原理 #

3.1 工作窃取 #

当P的本地队列为空时,从其他P窃取G:

text
P1: [G1, G2, G3]    P2: []
         │            │
         └────窃取────┘
P1: [G1, G2]        P2: [G3]

3.2 手递手传递 #

新创建的G优先放入本地队列:

go
go func() {}  // 放入当前P的本地队列

3.3 系统调用 #

当G进行系统调用时,M会阻塞,P会绑定到新的M:

text
G → 系统调用 → M阻塞 → P解绑 → 绑定新M → 继续执行

四、调度策略 #

4.1 调度时机 #

  • goroutine创建
  • goroutine退出
  • 系统调用
  • channel阻塞
  • time.Sleep
  • 主动让出

4.2 抢占式调度 #

Go 1.14+支持异步抢占:

go
func infiniteLoop() {
    for {
        // 可能被抢占
    }
}

4.3 公平调度 #

  • 本地队列:FIFO
  • 全局队列:定期检查
  • 系统调用返回的G优先

五、调度器配置 #

5.1 GOMAXPROCS #

go
runtime.GOMAXPROCS(4)  // 设置P的数量

5.2 查看调度信息 #

go
fmt.Println(runtime.GOMAXPROCS(0))  // P的数量
fmt.Println(runtime.NumCPU())       // CPU核心数
fmt.Println(runtime.NumGoroutine()) // G的数量

5.3 调试信息 #

go
import "runtime/debug"

debug.SetMaxThreads(100)  // 设置最大线程数

六、调度跟踪 #

6.1 GODEBUG #

bash
GODEBUG=schedtrace=1000 go run main.go

输出示例:

text
SCHED 1000ms: gomaxprocs=8 idleprocs=0 threads=10 spinningthreads=0 idlethreads=3 runqueue=0 [0 0 0 0 0 0 0 0]

6.2 go tool trace #

go
import (
    "os"
    "runtime/trace"
)

func main() {
    f, _ := os.Create("trace.out")
    defer f.Close()
    trace.Start(f)
    defer trace.Stop()
    
    // 程序代码
}

分析:

bash
go tool trace trace.out

七、调度器优化 #

7.1 避免频繁创建goroutine #

go
// 使用worker pool
type WorkerPool struct {
    tasks chan func()
}

func NewWorkerPool(n int) *WorkerPool {
    p := &WorkerPool{
        tasks: make(chan func(), 100),
    }
    for i := 0; i < n; i++ {
        go p.worker()
    }
    return p
}

func (p *WorkerPool) worker() {
    for task := range p.tasks {
        task()
    }
}

func (p *WorkerPool) Submit(task func()) {
    p.tasks <- task
}

7.2 控制并发数 #

go
func processWithLimit(items []int, limit int) {
    sem := make(chan struct{}, limit)
    var wg sync.WaitGroup
    
    for _, item := range items {
        wg.Add(1)
        sem <- struct{}{}
        go func(item int) {
            defer wg.Done()
            defer func() { <-sem }()
            process(item)
        }(item)
    }
    wg.Wait()
}

7.3 避免阻塞 #

go
// 使用非阻塞操作
select {
case ch <- data:
    // 发送成功
default:
    // 避免阻塞
}

八、调度器调优 #

8.1 CPU密集型 #

go
runtime.GOMAXPROCS(runtime.NumCPU())

8.2 IO密集型 #

go
// 增加goroutine数量
// 调度器会自动处理

8.3 混合型 #

go
// 根据实际情况调整
runtime.GOMAXPROCS(runtime.NumCPU())

九、调度器问题 #

9.1 goroutine泄漏 #

go
func leak() {
    ch := make(chan int)
    go func() {
        <-ch  // 永远阻塞
    }()
}

9.2 调度延迟 #

go
// 避免长时间占用CPU
for {
    // 定期让出
    runtime.Gosched()
}

9.3 线程过多 #

go
// 检查线程数
fmt.Println(runtime.ThreadCreateProfile(nil))

十、最佳实践 #

10.1 合理设置GOMAXPROCS #

go
// 容器环境
runtime.GOMAXPROCS(runtime.NumCPU())

10.2 使用worker pool #

go
// 复用goroutine
pool := NewWorkerPool(10)

10.3 避免goroutine泄漏 #

go
// 使用context取消
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

go func() {
    select {
    case <-ctx.Done():
        return
    case <-ch:
        // 处理
    }
}()

十一、总结 #

调度器要点:

组件 说明
G Goroutine
M 系统线程
P 处理器

关键点:

  1. GMP模型:理解G、M、P的关系
  2. 工作窃取:负载均衡机制
  3. 抢占调度:Go 1.14+支持异步抢占
  4. 调度跟踪:使用trace工具分析
  5. 性能调优:根据场景调整配置

准备好学习sync包了吗?让我们进入下一章!

最后更新:2026-03-26