文章

Go 并发模型深入

Go 并发模型深入

一句话:Go 的并发哲学是 CSP(Communicating Sequential Processes)——“不要通过共享内存来通信,要通过通信来共享内存”。goroutine + channel 把”等待 I/O 时不阻塞其他任务”这件难事做成了默认行为。

本文是 Go 基础速查 的深入版。速查讲”怎么用”,本文讲”为什么这样设计、怎么避免翻车”。

G-M-P 调度模型(理解一切的底座)

Go 调度器在用户态把 goroutine 映射到系统线程,三层:

graph TD
    G1[G: goroutine] --> M1[M: OS 线程]
    G2[G] --> M1
    G3[G] --> M2
    M1 --> P1[P: 逻辑处理器]
    M2 --> P2[P]
    P1 --> CPU1[CPU 核]
    P2 --> CPU2[CPU 核]
  • G(Goroutine):轻量协程,初始栈 2KB,按需增长。创建成本远低于线程
  • M(Machine):操作系统线程,真正跑代码的实体
  • P(Processor):逻辑处理器,持有可运行 G 的本地队列,GOMAXPROCS 决定 P 的数量(默认 = CPU 核数)

关键点:

  1. M 必须绑定 P 才能跑 G——P 是调度单元,决定并行度
  2. 本地队列 + 全局队列 + 工作窃取:P 本地空了去别的 P 偷,保证负载均衡
  3. syscall 阻塞时 M 与 P 解绑:一个 G 陷入系统调用,M 带着它脱离 P,P 立刻找另一个 M 继续跑其他 G——这是 Go “等待 I/O 不阻塞别人”的真相

这直接解释了你 Go 基础速查 那句”goroutine 不是因为要并行才用,是因为要在等一个东西时不阻塞另外一千个东西”——调度器在 syscall 时自动把 P 让出来。

channel:并发通信的主干

类型语义陷阱
无缓冲发送接收同时交接(同步点)单方先到会阻塞,易 deadlock
有缓冲缓冲未满/未空时不阻塞缓冲满仍阻塞;缓冲泄漏(没人读)
close关闭后读返回零值 + ok=false向已关闭 channel 发送 panic;重复 close panic

常用模式

// 1. Worker Pool:控制并发度(N 个 worker 消费任务)
jobs := make(chan Task, 100)
for i := 0; i < N; i++ {
    go func() {
        for job := range jobs { process(job) }  // range 自动在 channel 关闭后退出
    }()
}

// 2. Fan-out / Fan-in:多 goroutine 处理,再合并结果
// 3. Pipeline:channel 串联多个 stage
// 4. select:多 channel 多路复用 + default 非阻塞
select {
case msg := <-ch1:
    handle(msg)
case <-ctx.Done():   // 关键:用 context 控制退出
    return
default:
    // 非阻塞尝试
}

context:取消传播的命脉

context 解决”一个请求派生出多个 goroutine,怎么统一取消、怎么设超时”。

ctx, cancel := context.WithTimeout(parent, 3*time.Second)
defer cancel()   // 必须调用,否则泄漏

go func() {
    select {
    case <-ctx.Done():      // 超时/取消时 Done() 关闭
        return
    case res := <-work:
        deliver(res)
    }
}()
  • WithCancel / WithTimeout / WithDeadline / WithValue
  • 铁律cancel() 必须被调用(常配合 defer);goroutine 必须监听 <-ctx.Done() 否则泄漏
  • HTTP 与 WebSocket 连接模型与超时 里讲的”首包卡死”问题,本质就是没有 context 超时预算——Go HTTP client 的 context.WithTimeout 正是解法

sync 原语(共享内存的少数场景)

原语用途
sync.Mutex / RWMutex临界区保护(写少读多用 RWMutex)
sync.WaitGroup等一组 goroutine 结束
sync.Once单次初始化(如单例)
sync.Pool对象复用,降 GC 压力(见 Go 性能调优
sync.Map高并发读写 map(读多写少,避免锁)

并发四大陷阱(排障必看)

陷阱现象检测/解法
Data Race偶发数据错乱go run -race 检测;用 channel 或锁
Goroutine 泄漏内存/协程数只涨不跌监听 ctx.Done();pprof goroutine 看栈
Deadlock全部阻塞、程序卡死无缓冲 channel 单方等待;加超时
Context 不取消请求取消后后台仍跑所有派生 goroutine 必须接 ctx

关联知识

参考资源

  • Go Blog: “The Go scheduler” / “Go Concurrency Patterns”
  • Russ Cox 文章:Go 调度器设计

学习时间

内容日期状态
Go 并发模型2026-07-24完成:G-M-P、channel 模式、context、sync 原语、四大陷阱

状态

  • G-M-P 调度
  • channel 模式与陷阱
  • context 取消传播
  • sync 原语
  • 并发四大陷阱