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
上一篇 13小时前
下一篇 21小时前

相关推荐

  • Go工程师体系课 015

    Docker 容器化 —— Go 项目实战指南 一、Docker 核心概念 1.1 什么是 Docker Docker 是一个开源的容器化平台,它可以将应用程序及其所有依赖项打包到一个标准化的单元(容器)中,从而实现"一次构建,到处运行"。对于 Go 开发者而言,Docker 解决了以下痛点: 开发环境与生产环境不一致 依赖管理复杂(数据库、缓存、消息队列等…

  • Go工程师体系课 008

    订单及购物车 先从库存服务中将 srv 的服务代码框架复制过来,查找替换对应的名称(order_srv) 加密技术基础 对称加密(Symmetric Encryption) 原理: 使用同一个密钥进行加密和解密 就像一把钥匙,既能锁门也能开门 加密速度快,适合大量数据传输 使用场景: 本地文件加密 数据库内容加密 大量数据传输时的内容加密 内部系统间的快速通…

    后端开发 8小时前
    100
  • 编程基础 0008_标准库进阶

    Go 标准库进阶 系统整理 Go 标准库中最常用的包,重点覆盖 io、os、bufio、strings、time、fmt 等 1. io 包核心接口 Go 的 I/O 设计围绕几个核心接口展开,几乎所有 I/O 操作都基于它们。 // 最基础的两个接口 type Reader interface { Read(p []byte) (n int, err er…

    后端开发 19小时前
    500
  • Go工程师体系课 019

    Go 内存模型与 GC 1. 内存分配基础 1.1 栈(Stack)与堆(Heap) ┌─────────────────────────────┐ │ 堆 (Heap) │ ← 动态分配,GC 管理 │ ┌─────┐ ┌─────┐ ┌─────┐ │ │ │ obj │ │ obj │ │ obj │ │ │ └─────┘ └─────┘ └────…

  • Go资深工程师讲解(慕课) 007_godoc与代码生成

    Go 文档生成与示例代码 对应视频 8-6 生成文档和示例代码 1. godoc 文档生成 Go 的文档直接从源码注释中提取,不需要特殊标记语法。 1.1 注释规范 // Package queue 实现了一个简单的 FIFO 队列。 // // 该队列基于切片实现,支持 Push、Pop 和 IsEmpty 操作。 package queue // Que…

简体中文 繁体中文 English