程序员面试宝典

一站式面试准备平台

返回分类
Go高级

Go Goroutine 与 GMP 调度模型

深入理解 Go 协程的 GMP 调度模型、运行时机制及性能优化

2026-03-27
阅读时间: 11分钟

Go Goroutine 与 GMP 调度模型

Go 的并发模型是其最强大的特性之一。理解 GMP 调度模型对于写出高性能 Go 程序至关重要。

Goroutine 基础

什么是 Goroutine

Goroutine 是 Go 轻量级的协程,由 Go 运行时管理:

go
// 启动一个 goroutine
go func() {
    fmt.Println("Hello from goroutine")
}()

// 普通函数也可以用 go 启动
go doSomething()

Goroutine vs 线程

特性线程Goroutine
创建成本~1MB 栈~2KB 栈(可动态增长)
创建速度极快
调度操作系统(内核态)Go 运行时(用户态)
切换成本用户态→内核态用户态内切换

Goroutine 的栈空间

Goroutine 的栈是动态增长的:

go
func recursion() {
    // 初始栈大小约 2KB
    // 当栈空间不足时,会自动扩容(最多 1GB)
    recursion()
}

GMP 调度模型

GMP 架构

                        ┌─────────────────┐
                        │     Goroutines   │
                        │   (待执行的 Go)   │
                        └────────┬──────────┘
                                 │
                                 ▼
┌──────────────────────────────────────────────────────────┐
│                    OS (Operating System)                 │
│  ┌────────────────────────────────────────────────────┐  │
│  │                   M (Machine/Thread)                │  │
│  │  ┌──────────────────────────────────────────────┐  │  │
│  │  │              P (Processor)                   │  │  │
│  │  │  ┌─────────┐ ┌─────────┐ ┌─────────┐        │  │  │
│  │  │  │  G 1    │ │  G 2    │ │  G 3    │ ...   │  │  │
│  │  │  │(gorout.)│ │(gorout.)│ │(gorout.)│        │  │  │
│  │  │  └─────────┘ └─────────┘ └─────────┘        │  │  │
│  │  │          本地运行队列 (LRQ)                 │  │  │
│  │  └──────────────────────────────────────────────┘  │  │
│  └────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────┘
         ▲                                    ▲
         │                                    │
         └──────── 全局运行队列 (GRQ) ─────────┘
  • G (Goroutine):Go 协程,包含执行栈和调度信息
  • M (Machine):操作系统线程,实际执行代码
  • P (Processor):逻辑处理器,有本地运行队列

GMP 工作流程

go
// 启动 Go 程序
func main() {
    // 1. 创建 P(默认 GOMAXPROCS 个)
    // 2. 创建 M(内核线程)
    // 3. 创建 main goroutine 在 P 上运行
    
    for i := 0; i < 10; i++ {
        go fmt.Println(i)  // 创建新 goroutine,加入 P 的本地队列
    }
    
    time.Sleep(time.Second)
}

调度过程

  1. M 从本地队列获取 GM → P → G
  2. 本地队列为空时:从全局队列或其他 P 偷取
  3. G 阻塞时:M 释放 P,寻找其他 G 执行
  4. G 完成后:放回队列或销毁

关键调度机制

Work Stealing(工作窃取)

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

go
// 源码简化逻辑
func (p *Processor) run() {
    for {
        if p.lrq.size > 0 {
            g := p.lrq.pop()
            execute(g)
        } else {
            // 尝试从全局队列获取
            g := globalRunQueue.pop()
            if g != nil {
                execute(g)
            } else {
                // 从其他 P 偷取
                g = stealFromOtherP()
            }
        }
    }
}

Hand off(移交)

当 M 阻塞时(如系统调用),释放 P 给其他 M:

G1 执行中 → 发起系统调用 → M1 阻塞
                                    ↓
                              M2 接管 P

Goroutine 阻塞分类

阻塞类型M 是否释放 P说明
系统调用网络 I/O、文件 I/O
channel 操作等待数据
mutex 等待等待锁
time.Sleep休眠

channel 与调度

channel 的阻塞行为

go
// 发送阻塞:队列满时
ch := make(chan int, 3)
for i := 0; i < 100; i++ {
    ch <- i  // 超过3个会阻塞
}

// 接收阻塞:队列空时
data := <-ch  // 无数据时阻塞

select 与调度

go
select {
case v := <-ch1:
    fmt.Println("收到 ch1:", v)
case v := <-ch2:
    fmt.Println("收到 ch2:", v)
case <-time.After(time.Second):
    fmt.Println("超时")
default:
    fmt.Println("默认分支")
}

常见面试问题

Q1: GMP 模型中 P 的数量是多少?

默认等于 CPU 核心数:

go
func main() {
    // 查看 CPU 核心数
    fmt.Println(runtime.NumCPU())      // 如:8
    
    // 查看默认 P 数量
    fmt.Println(runtime.GOMAXPROCS(0))  // 默认等于 NumCPU
    
    // 动态修改 P 数量(不建议在生产环境修改)
    runtime.GOMAXPROCS(4)
}

Q2: Goroutine 泄漏如何排查?

Goroutine 泄漏通常由 channel 阻塞引起:

go
// 泄漏示例
func leak() {
    ch := make(chan int)
    
    go func() {
        ch <- 42  // 发送后永不接收
    }()
    
    return  // leak() 返回后,goroutine 永远阻塞
}

// 解决:使用 context 或 select
func noLeak(ctx context.Context) {
    ch := make(chan int)
    
    go func() {
        select {
        case ch <- 42:
        case <-ctx.Done():
            return
        }
    }()
    
    return
}

Q3: 如何控制并发数量?

使用 worker pool 模式:

go
func workerPool(n int) {
    ch := make(chan int, 100)
    
    // 启动 n 个 worker
    var wg sync.WaitGroup
    for i := 0; i < n; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            for task := range ch {
                process(task)
            }
        }(i)
    }
    
    // 发送任务
    for i := 0; i < 1000; i++ {
        ch <- i
    }
    close(ch)
    
    wg.Wait()
}

Q4: runtime.Gosched() 的作用?

主动让出 CPU,允许其他 goroutine 执行:

go
func main() {
    go func() {
        for i := 0; i < 5; i++ {
            fmt.Print("A")
            runtime.Gosched()  // 让出执行权
        }
    }()
    
    for i := 0; i < 5; i++ {
        fmt.Print("B")
        runtime.Gosched()
    }
    
    time.Sleep(time.Second)
}

性能优化建议

  1. 避免大量创建 Goroutine:使用 worker pool
  2. 合理设置 GOMAXPROCS:通常默认即可
  3. 避免 shared memory 通信:使用 channel
  4. 注意 channel 容量:减少阻塞
  5. 使用 sync.Pool:复用临时对象
go
// 复用对象,减少 GC 压力
var bufPool = sync.Pool{
    New: func() interface{} {
        b := make([]byte, 1024)
        return &b
    },
}

func process() {
    buf := bufPool.Get().(*[]byte)
    defer bufPool.Put(buf)
    // 使用 buf
}

相关标签