Go资深工程师讲解(慕课) 008_GMP调度器与Go设计哲学

Go GMP 调度器与设计哲学

对应视频 9-2 go语言的调度器、18-1 体会Go语言的设计、18-2 课程总结

1. Go 调度器演进

1.0 时代:单线程调度器(Go 0.x)

  • 只有一个线程运行 goroutine
  • 所有 goroutine 排队等待
  • 无法利用多核

1.1 时代:多线程调度器(Go 1.0)

  • 引入多线程
  • 但全局锁竞争严重,性能瓶颈

1.2+ 时代:GMP 模型(Go 1.2 至今)

引入了著名的 GMP 调度模型

2. GMP 模型详解

    G (Goroutine)     - 协程,用户级轻量线程
    M (Machine/Thread) - 操作系统线程
    P (Processor)      - 逻辑处理器,调度上下文

2.1 三者的关系

                   ┌─────────┐
                   │ Go 程序  │
                   └────┬────┘
                        │ 创建 goroutine
           ┌────────────┼────────────┐
           ▼            ▼            ▼
        ┌─────┐     ┌─────┐     ┌─────┐
        │  G  │     │  G  │     │  G  │    ... (成千上万个)
        └──┬──┘     └──┬──┘     └──┬──┘
           │           │           │
     ┌─────┴─────┐     │     ┌────┴─────┐
     │ P 的本地队列│     │     │ P 的本地队列│
     │ [G][G][G] │     │     │ [G][G]   │
     └─────┬─────┘     │     └────┬─────┘
           │           │          │
        ┌──┴──┐     ┌──┴──┐   ┌──┴──┐
        │  P  │     │  P  │   │  P  │    (GOMAXPROCS 个)
        └──┬──┘     └──┬──┘   └──┬──┘
           │           │          │
        ┌──┴──┐     ┌──┴──┐   ┌──┴──┐
        │  M  │     │  M  │   │  M  │    (按需创建)
        └──┬──┘     └──┬──┘   └──┬──┘
           │           │          │
     ══════╧═══════════╧══════════╧══════
              操作系统内核线程

2.2 各组件详解

G (Goroutine)
- 初始栈大小仅 2KB(线程通常 1-8MB)
- 栈可动态增长和收缩
- 包含执行的函数、栈指针、程序计数器等
- 状态:可运行、运行中、等待中、已完成

M (Machine)
- 对应一个操作系统线程
- 由 Go runtime 按需创建,默认最多 10000 个
- M 必须持有 P 才能运行 G
- 当 M 因系统调用阻塞时,P 会被抢走给其他 M

P (Processor)
- 数量由 GOMAXPROCS 决定(默认等于 CPU 核数)
- 每个 P 有一个本地运行队列(最多 256 个 G)
- P 是调度的核心,决定哪个 G 在哪个 M 上运行

2.3 调度策略

// 查看和设置 P 的数量
runtime.GOMAXPROCS(0)    // 获取当前值
runtime.GOMAXPROCS(4)    // 设置为 4
runtime.NumCPU()         // CPU 核数

调度时机(goroutine 可能的切换点):

切换点 说明
I/O 操作 文件、网络读写
channel 操作 发送/接收阻塞时
select 多路复用
等待锁 sync.Mutex 等
函数调用 编译器在函数入口插入检查点
runtime.Gosched() 手动让出
GC 垃圾回收的 STW 阶段
系统调用 syscall 阻塞时 M 和 P 分离

2.4 Work Stealing(工作窃取)

当一个 P 的本地队列为空时:

  1. 先从全局队列取 G
  2. 全局队列也没有 → 随机从其他 P 的队列偷一半 G
  3. 都没有 → 检查网络轮询器
  4. 还没有 → M 休眠
    P1 [空]           P2 [G5 G6 G7 G8]
       │                    │
       │   偷一半!          │
       │ ◄───── steal ───── │
       │                    │
    P1 [G7 G8]        P2 [G5 G6]

2.5 系统调用处理(Hand Off)

当 G 执行系统调用(如文件 I/O)阻塞 M 时:

  正常状态:     M1 ──── P1 ──── G1(syscall阻塞)

  Hand Off:    M1 ──── G1(继续阻塞在syscall)
               M2 ──── P1 ──── G2(P被转给新的M)

P 会被转给空闲的 M(或新建一个 M),不让 P 闲着。

2.6 网络轮询器(netpoller)

网络 I/O 不会阻塞 M,而是使用 epoll/kqueue 异步处理:

  1. G 发起网络调用 → G 被挂到 netpoller
  2. M 不阻塞,继续运行其他 G
  3. 网络就绪时 → G 被放回可运行队列

这就是为什么 Go 能高效处理大量网络连接。

3. Goroutine 与线程的对比

特性 Goroutine OS Thread
栈大小 2KB(动态增长) 1-8MB(固定)
创建成本 ~0.3μs ~30μs
切换成本 ~0.2μs(用户态) ~1μs(内核态)
调度方式 协作式+抢占式 抢占式
数量级 百万级 千级
通信方式 channel 共享内存+锁

抢占式调度(Go 1.14+)

Go 1.14 之前是纯协作式调度,密集计算的 goroutine 可能长时间占用 M:

// Go 1.14 之前,这个会独占线程
go func() {
    for {} // 死循环,不让出
}()

Go 1.14+ 引入了基于信号的抢占(SIGURG),即使没有函数调用也能被抢占。

4. 实际观察调度器

# GODEBUG 查看调度器状态
GODEBUG=schedtrace=1000 go run main.go
# 每 1000ms 输出调度器状态

# 更详细
GODEBUG=schedtrace=1000,scheddetail=1 go run main.go

# go tool trace 可视化
// 在代码中查看
fmt.Println("goroutine数:", runtime.NumGoroutine())
fmt.Println("CPU核数:", runtime.NumCPU())
fmt.Println("GOMAXPROCS:", runtime.GOMAXPROCS(0))

5. Go 语言的设计哲学

对应视频 Ch18:体会Go语言的设计

5.1 少即是多(Less is more)

  • 只有 25 个关键字(C 有 32 个,Java 有 50+)
  • 没有 class,用 struct + method 替代
  • 没有继承,用组合(embedding)替代
  • 没有泛型(Go 1.18 前)—— 保持简单
  • 没有异常,用多返回值 + error 替代 try/catch
  • 只有 for 循环,没有 while/do-while
  • 没有三元运算符 ?:

5.2 组合优于继承

// 不是继承,是组合
type Animal struct {
    Name string
}
func (a Animal) Speak() string { return a.Name + " speaks" }

type Dog struct {
    Animal  // 嵌入,不是继承
    Breed string
}
// Dog 自动获得 Speak 方法,但可以覆盖

5.3 接口的隐式实现

// 不需要 implements 关键字
type Stringer interface {
    String() string
}
// 任何有 String() string 方法的类型自动实现了 Stringer
// → 解耦了定义者和实现者

5.4 并发是一等公民

  • go 关键字启动协程(不是库函数)
  • chan 是内置类型(不是库中的 Queue)
  • select 是语言级别的多路复用
  • "Don't communicate by sharing memory; share memory by communicating."

5.5 工具链哲学

工具 作用
gofmt 统一代码风格,没有风格战争
go vet 静态分析,找常见错误
go test 内置测试框架,不需要第三方
go doc 注释即文档
go build 编译成单一二进制,无依赖
go mod 内置依赖管理

5.6 错误处理哲学

// 显式处理每个错误,不会被隐藏
f, err := os.Open("file.txt")
if err != nil {
    // 必须处理
}
// 虽然"啰嗦",但:
// 1. 错误路径清晰可见
// 2. 不会因为忘记 catch 而崩溃
// 3. 强迫你思考每个可能的失败

5.7 Go 箴言(Go Proverbs by Rob Pike)

箴言 含义
Don't communicate by sharing memory, share memory by communicating 用 channel 而非锁
Concurrency is not parallelism 并发是结构,并行是执行
Channels orchestrate; mutexes serialize channel 编排流程,mutex 串行化访问
The bigger the interface, the weaker the abstraction 接口越小越好
Make the zero value useful 零值应该可直接使用
interface{} says nothing 空接口不表达任何信息
Gofmt's style is no one's favorite, yet gofmt is everyone's favorite 统一风格比好看重要
A little copying is better than a little dependency 少量复制好过引入依赖
Clear is better than clever 清晰胜过聪明
Errors are values 错误是值,可以编程处理
Don't just check errors, handle them gracefully 优雅处理错误,不只是检查
Don't panic 不要轻易 panic

6. 课程总结思维导图

Go 语言核心
├── 基础语法
│   ├── 变量、常量、类型
│   ├── 控制流(if/for/switch)
│   └── 函数(多返回值、闭包、defer)
├── 面向对象
│   ├── struct + method(代替 class)
│   ├── 组合(代替继承)
│   └── interface(隐式实现、duck typing)
├── 函数式编程
│   ├── 闭包、高阶函数
│   ├── 装饰器/中间件模式
│   └── Functional Options
├── 错误处理
│   ├── error 接口、多返回值
│   ├── panic/recover(仅用于不可恢复错误)
│   └── 统一错误处理(errWrapper 模式)
├── 测试
│   ├── 表格驱动测试
│   ├── 性能测试(Benchmark)
│   ├── pprof 性能分析
│   └── Example 文档测试
├── 并发编程
│   ├── goroutine(GMP 调度模型)
│   ├── channel(CSP 通信模型)
│   ├── select(多路复用)
│   └── sync 包(WaitGroup、Mutex)
├── 标准库
│   ├── net/http
│   ├── encoding/json
│   └── html/template
└── 工程实践
    ├── 单任务→并发→分布式爬虫
    ├── ElasticSearch 集成
    ├── Docker 容器化
    └── RPC 分布式通信

主题测试文章,只做测试使用。发布者:Walker,转转请注明出处:https://walker-learn.xyz/archives/6752

(0)
Walker的头像Walker
上一篇 2026年3月8日 15:11
下一篇 2026年3月9日 12:56

相关推荐

  • Go工程师体系课 009

    其它一些功能 个人中心 收藏 管理收货地址(增删改查) 留言 拷贝inventory_srv--> userop_srv 查询替换所有的inventory Elasticsearch 深度解析文档 1. 什么是Elasticsearch Elasticsearch是一个基于Apache Lucene构建的分布式、RESTful搜索和分析引擎,能够快速地…

    后端开发 2026年3月6日
    5400
  • Go工程师体系课 002

    GOPATH 与 Go Modules 的区别 1. 概念 GOPATH 是 Go 的早期依赖管理机制。 所有的 Go 项目和依赖包必须放在 GOPATH 目录中(默认是 ~/go)。 一定要设置 GO111MODULE=off 项目路径必须按照 src/包名 的结构组织。 不支持版本控制,依赖管理需要手动处理(例如 go get)。 查找依赖包的顺序是 g…

    2026年3月6日
    5700
  • Go工程师体系课 009

    其它一些功能 个人中心 收藏 管理收货地址(增删改查) 留言 拷贝inventory_srv--> userop_srv 查询替换所有的inventory Elasticsearch 深度解析文档 1. 什么是Elasticsearch Elasticsearch是一个基于Apache Lucene构建的分布式、RESTful搜索和分析引擎,能够快速地…

    后端开发 2026年3月7日
    6000
  • Go工程师体系课 014

    rocketmq 快速入门 去我们的各种配置(podman)看是怎么安装的 概念介绍 RocketMQ 是阿里开源、Apache 顶级项目的分布式消息中间件,核心组件: NameServer:服务发现与路由 Broker:消息存储、投递、拉取 Producer:消息生产者(发送消息) Consumer:消息消费者(订阅并消费消息) Topic/Tag:主题/…

    后端开发 2026年3月7日
    14100
  • Go资深工程师讲解(慕课) 003

    003 测试 吐槽别人家的,go语言采用表格驱动测试 测试数据和测试逻辑混在一些 出错信息不明确 一旦一个数据出错测试全部结束 表格驱动测试 test:=[]struct{ a,b,c int32 }{ {1,2,3}, {0,2,0}, {0,0,0}, {0,0,0}, {-1,1,0}, {math.MaxInt32,1,math.MinInt32},…

    后端开发 2026年3月6日
    4800
简体中文 繁体中文 English