1. 并发基础:Goroutine 与并发模型概述

1.1 并发与并行的本质区别

在 Go 语言性能优化中,理解并发(Concurrency)与并行(Parallelism)的区别是构建高性能系统的第一道门槛。并发是程序的结构属性,指多个任务在逻辑上同时执行;并行是程序的执行属性,指多个任务在物理上同时运行。

维度 并发(Concurrency) 并行(Parallelism)
核心定义 任务交替执行,逻辑上同时 任务真正同时运行,物理上同时
硬件依赖 单核 CPU 即可实现 必须多核 CPU 或多处理器
关注点 任务分解与协调 吞吐量与加速比
Go 实现 Goroutine + Channel GOMAXPROCS > 1
典型场景 Web 服务处理多个请求 大规模矩阵运算
性能优化启示:并发是设计问题,并行是执行问题。先设计正确的并发结构,再通过调整 GOMAXPROCS 获得并行加速。盲目增加并行度不会修复糟糕的并发设计。

1.2 Goroutine:轻量级并发单元

Goroutine 是 Go 语言并发编程的核心抽象,它比操作系统线程轻量得多。一个 Goroutine 的初始栈仅 2KB,而线程栈通常为 1MB 以上。这意味着你可以轻松创建数十万个 Goroutine 而不会耗尽内存。

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

// 使用 WaitGroup 等待完成
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
    wg.Add(1)
    go func(id int) {
        defer wg.Done()
        fmt.Printf("Goroutine %d 执行\n", id)
    }(i)
}
wg.Wait()
关键指标对比:
  • Goroutine 创建成本:约 4KB 内存,纳秒级
  • OS 线程创建成本:约 1MB 内存,微秒级
  • 上下文切换成本:Goroutine 约 50ns,线程约 1-10μs

1.3 GMP 调度原理

Go 运行时实现了 M:N 调度模型,将 M 个 Goroutine 映射到 N 个 OS 线程上执行。GMP 是调度器的三个核心组件:

  • G(Goroutine):代表一个待执行的任务,包含栈、程序计数器、状态等信息。
  • M(Machine):代表 OS 线程,负责执行 Goroutine 的代码。
  • P(Processor):代表逻辑处理器,是 G 和 M 之间的调度上下文,持有本地运行队列。

GMP 调度器的核心设计目标是:减少线程阻塞和上下文切换,充分利用多核 CPU。

GMP 调度器架构图 P0(逻辑处理器) 本地运行队列 G1 → G2 → G3 → G4 P1(逻辑处理器) 本地运行队列 G5 → G6 → G7 P2(逻辑处理器) 本地运行队列 G8 → G9 M0(OS 线程) 执行 G1 状态:运行中 M1(OS 线程) 执行 G5 状态:运行中 M2(OS 线程) 执行 G8 状态:运行中 全局运行队列(Global Queue) G10 → G11 → G12 → G13 → G14 P 从全局队列 窃取 G 到本地队列

1.4 调度流程与工作窃取

GMP 调度器的工作流程遵循以下步骤:

  1. 创建 G:通过 go 关键字创建 Goroutine,初始放入 P 的本地队列或全局队列。
  2. 绑定 P 与 M:每个 P 绑定一个 M,M 从 P 的本地队列获取 G 执行。
  3. 工作窃取(Work Stealing):当 P 的本地队列为空时,从其他 P 的队列尾部窃取一半 G,或从全局队列获取。
  4. 系统调用处理:G 进入系统调用时,P 与 M 解绑,P 寻找空闲 M 继续执行其他 G。
  5. 抢占式调度:Go 1.14 后引入基于信号的抢占,防止长时间运行的 G 阻塞其他 G。
性能陷阱:当 Goroutine 数量远大于 P 的数量时,调度开销会显著增加。建议将 Goroutine 数量控制在合理范围内,避免创建过多 Goroutine 导致调度器过载。

1.5 并发模型对比

模型 代表语言 通信方式 共享内存 调度粒度
CSP(通信顺序进程) Go Channel 不共享,通过通信共享 Goroutine 级
Actor 模型 Erlang, Akka 消息传递 完全不共享 进程级
共享内存 + 锁 Java, C++ 共享变量 + 锁 直接共享 线程级
Future/Promise Scala, JavaScript 回调/链式调用 不直接共享 任务级
Go 的设计哲学:"Do not communicate by sharing memory; instead, share memory by communicating."(不要通过共享内存来通信,而应通过通信来共享内存。)这是 Go 并发模型区别于传统锁模型的核心思想。

1.6 GOMAXPROCS 与并行控制

GOMAXPROCS 控制同时执行用户级 Go 代码的 OS 线程数量,即 P 的数量。默认值为 CPU 核心数,但可以通过环境变量或 runtime.GOMAXPROCS() 调整。

// 获取当前 GOMAXPROCS
current := runtime.GOMAXPROCS(0)
fmt.Printf("当前 GOMAXPROCS: %d\n", current)

// 设置 GOMAXPROCS 为 4
runtime.GOMAXPROCS(4)

// 在 CPU 密集型任务中,GOMAXPROCS 应接近 CPU 核心数
// 在 I/O 密集型任务中,可以适当增加 Goroutine 数量
GOMAXPROCS 对并发与并行的影响 GOMAXPROCS = 1(单核) 并发:Goroutine 交替执行 并行:无,同一时刻只执行一个 G 适用:I/O 密集型、网络服务 GOMAXPROCS = N(多核) 并发:Goroutine 逻辑同时执行 并行:最多 N 个 G 同时执行 适用:CPU 密集型、计算任务 性能优化建议 • CPU 密集型:GOMAXPROCS = CPU 核心数 • I/O 密集型:GOMAXPROCS 可略高于核心数 • 避免设置过大,否则增加调度开销

1.7 本章小结

并发基础是理解 Go 性能优化的起点。Goroutine 的轻量特性使得大规模并发成为可能,GMP 调度器通过工作窃取和抢占式调度实现了高效的 M:N 映射。理解并发与并行的区别,有助于在后续章节中正确分析锁竞争和原子操作的性能影响。

核心要点回顾:
  • 并发是结构,并行是执行;先设计并发,再优化并行。
  • Goroutine 初始栈 2KB,创建成本极低,适合大规模使用。
  • GMP 调度器通过 P 的本地队列和全局队列实现高效调度。
  • 工作窃取机制平衡负载,减少线程阻塞。
  • GOMAXPROCS 控制并行度,需根据任务类型合理设置。