Goroutine与GPM调度模型
2025/12/11大约 4 分钟
当然可以!以下是专为你定制的学习实践指南,完全基于你的开发环境:
- 操作系统:Linux Mint XFCE
- Go 版本:go1.22.2 linux/amd64
- 项目目录:
/home/liumangmang/GolandProjects
📌 标题:
Go 并发入门:Goroutine 基础与 GPM 调度模型实战解析
✅ 步骤 1:创建练习项目
打开终端,执行:
cd /home/liumangmang/GolandProjects
mkdir go-goroutine-gpm && cd go-goroutine-gpm
go mod init go-goroutine-gpm✅ 步骤 2:编写 Goroutine 基础示例(main.go)
创建 main.go:
nano main.go粘贴以下代码,涵盖 goroutine 启动、并发行为、以及 GPM 模型的观察方法:
package main
import (
"fmt"
"runtime"
"sync"
"time"
)
func say(s string, id int) {
for i := 0; i < 3; i++ {
fmt.Printf("[%d] %s: step %d\n", id, s, i)
time.Sleep(100 * time.Millisecond) // 模拟工作
}
}
func main() {
fmt.Println("=== 1. 单线程顺序执行 ===")
say("Hello", 1)
say("World", 2)
fmt.Println("\n=== 2. 使用 Goroutine 并发执行 ===")
go say("Goroutine-A", 1)
go say("Goroutine-B", 2)
// 主 goroutine 等待 1 秒,否则程序会提前退出
time.Sleep(1 * time.Second)
fmt.Println("\n=== 3. 使用 sync.WaitGroup 安全等待 ===")
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
fmt.Printf("Worker %d started\n", id)
time.Sleep(time.Duration(id*200) * time.Millisecond)
fmt.Printf("Worker %d finished\n", id)
}(i)
}
wg.Wait() // 阻塞直到所有 goroutine 完成
fmt.Println("All workers done!")
fmt.Println("\n=== 4. 查看当前 GOMAXPROCS 和 CPU 核心数 ===")
fmt.Printf("CPU 核心数: %d\n", runtime.NumCPU())
fmt.Printf("GOMAXPROCS: %d\n", runtime.GOMAXPROCS(0)) // 0 表示不修改,仅查询
fmt.Println("\n=== 5. 手动设置 GOMAXPROCS(通常不需要)===")
old := runtime.GOMAXPROCS(2)
fmt.Printf("旧 GOMAXPROCS: %d, 新设为: 2\n", old)
runtime.GOMAXPROCS(old) // 恢复原值
fmt.Println("\n=== 6. 观察 Goroutine 数量变化 ===")
fmt.Printf("启动前 Goroutine 数: %d\n", runtime.NumGoroutine())
var wg2 sync.WaitGroup
for i := 0; i < 5; i++ {
wg2.Add(1)
go func(n int) {
defer wg2.Done()
time.Sleep(200 * time.Millisecond)
}(i)
}
time.Sleep(50 * time.Millisecond) // 让 goroutine 启动
fmt.Printf("启动后 Goroutine 数: %d\n", runtime.NumGoroutine())
wg2.Wait()
fmt.Printf("全部完成后 Goroutine 数: %d\n", runtime.NumGoroutine())
}保存并退出(Ctrl+O → Enter → Ctrl+X)。
✅ 步骤 3:格式化并运行
go fmt
go run .预期输出(顺序可能略有不同):
=== 1. 单线程顺序执行 ===
[1] Hello: step 0
[1] Hello: step 1
[1] Hello: step 2
[2] World: step 0
[2] World: step 1
[2] World: step 2
=== 2. 使用 Goroutine 并发执行 ===
[Goroutine-A] step 0
[Goroutine-B] step 0
[Goroutine-A] step 1
[Goroutine-B] step 1
[Goroutine-A] step 2
[Goroutine-B] step 2
=== 3. 使用 sync.WaitGroup 安全等待 ===
Worker 1 started
Worker 2 started
Worker 3 started
Worker 1 finished
Worker 2 finished
Worker 3 finished
All workers done!
=== 4. 查看当前 GOMAXPROCS 和 CPU 核心数 ===
CPU 核心数: 8
GOMAXPROCS: 8
=== 5. 手动设置 GOMAXPROCS(通常不需要)===
旧 GOMAXPROCS: 8, 新设为: 2
=== 6. 观察 Goroutine 数量变化 ===
启动前 Goroutine 数: 1
启动后 Goroutine 数: 6
全部完成后 Goroutine 数: 1💡 注意:Goroutine 输出顺序是不确定的,这正是并发的体现!
🔍 核心概念解析:GPM 调度模型
Go 的调度器采用 GPM 模型,三者含义如下:
| 组件 | 全称 | 作用 |
|---|---|---|
| G | Goroutine | 用户级轻量协程,由 Go 运行时管理 |
| P | Processor | 逻辑处理器,持有 G 的本地队列,数量 = GOMAXPROCS |
| M | Machine | 操作系统线程,真正执行代码的实体 |
调度流程简述:
- 每个 P 绑定一个 M(OS 线程)
- G 被分配到某个 P 的本地队列
- M 执行 P 上的 G
- 若 G 阻塞(如 I/O),M 会与 P 解绑,P 可被其他 M 接管,保证 CPU 不空闲
✅ 优势:
- Goroutine 创建成本极低(初始栈仅 2KB)
- 即使百万级 Goroutine,也能高效调度
- 阻塞不会阻塞 OS 线程(网络 I/O 由 netpoller 处理)
🧪 实验建议(在 GoLand 中)
查看 Goroutine 堆栈:
在调试模式下,点击 Goroutines 面板,实时查看所有 goroutine 状态。修改 GOMAXPROCS:
尝试runtime.GOMAXPROCS(1),观察并发是否变成“伪并发”(仍能切换,但只用 1 个 CPU)。制造阻塞:
在 goroutine 中加入time.Sleep或http.Get,观察调度器如何复用 M。
⚠️ 常见误区提醒
| 误区 | 正确理解 |
|---|---|
| “Goroutine = OS 线程” | ❌ Goroutine 是用户态协程,由 Go 调度器在少量 OS 线程上复用 |
| “必须设置 GOMAXPROCS” | ❌ Go 1.5+ 默认等于 CPU 核心数,通常无需手动设置 |
| “Goroutine 会自动等待” | ❌ 主 goroutine 退出,程序立即终止!必须用 WaitGroup、channel 或 time.Sleep 等待 |
📚 延伸学习方向
- 使用
go tool trace可视化调度行为 - 学习 channel 实现 goroutine 通信(避免共享内存)
- 理解 context 如何优雅取消 goroutine
如果你希望我接下来带你实现 channel 通信、select 多路复用、或 worker pool 模式,欢迎随时告诉我!祝你轻松掌握 Go 并发编程 🚀
