并发编程知识体系概览
什么是 Go 并发?
Go 的并发编程基于 CSP(Communicating Sequential Processes) 模型——通过通信来共享内存,而不是通过共享内存来通信。这是 Go 与 Java、C++ 等语言并发模型最根本的区别。
"Do not communicate by sharing memory; instead, share memory by communicating." — Go Proverb
Go 提供了两个语言级并发原语:
- Goroutine:轻量级协程,比 OS 线程轻 1000 倍(初始栈 2-8KB)
- Channel:类型安全的消息传递管道
为什么 Go 的并发这么强?
| 维度 | Go Goroutine | Java Thread | Linux Thread |
|---|---|---|---|
| 创建开销 | ~2KB 栈 | ~512KB-1MB 栈 | ~8MB 栈 |
| 切换开销 | ~几十 ns(用户态) | ~几 μs(内核态) | ~几 μs(内核态) |
| 调度方式 | Go runtime 调度(M:N) | OS 内核调度(1:1) | OS 内核调度 |
| 可同时创建数量 | 百万级 | 千到万级 | 千到万级 |
| 通信方式 | Channel(首选) + sync | synchronized + Lock | mutex + semaphore |
核心知识点
GMP 调度模型
Go 的调度器使用 GMP 模型实现 goroutine 到 OS 线程的映射:
- G(Goroutine):用户态协程,携带函数调用栈
- M(Machine):OS 线程,真正执行代码的载体
- P(Processor):逻辑处理器,维护本地 G 队列,数量默认等于 CPU 核数(
GOMAXPROCS)
调度核心机制:
- 本地队列:每个 P 有本地运行队列(最多 256 个 G),减少锁竞争
- 全局队列:本地队列满时溢出到全局队列
- 工作窃取(Work Stealing):P 的本地队列空时,从其他 P 或全局队列窃取 G
- 抢占式调度:Go 1.14 引入基于信号的异步抢占,避免单个 G 长期占用
Channel——goroutine 间的管道
Channel 是 Go 并发的核心通信机制,类型安全、线程安全:
// 无缓冲 channel:发送和接收同步(握手)
ch := make(chan int)
// 有缓冲 channel:缓冲区满时发送阻塞,空时接收阻塞
ch := make(chan int, 10)
// 单向 channel(用于函数签名约束)
func producer(out chan<- int) { out <- 42 }
func consumer(in <-chan int) { val := <-in }
Channel 的关闭语义是重要面试题——详见 Channel 详解。
sync 包——互斥与同步
虽然 Go 推崇 Channel,但 sync 包在保护共享状态时更高效:
| 工具 | 用途 |
|---|---|
sync.Mutex | 互斥锁 |
sync.RWMutex | 读写锁(多读一写) |
sync.WaitGroup | 等待一组 goroutine 完成 |
sync.Once | 只执行一次(单例) |
sync.Map | 并发安全 map |
sync.Cond | 条件变量 |
sync.Pool | 对象池(减少 GC 压力) |
Context——传递取消信号和截止时间
context.Context 是 Go 并发编程的"黏合剂",用于:
- 取消信号传播:上游取消时,下游所有 goroutine 收到通知
- 截止时间:设置超时,避免 goroutine 无限等待
- 传递请求范围的值:如 RequestID、UserID
详见 Context 详解。
并发模式
Go 有一套成熟的并发模式:
| 模式 | 说明 | 适用场景 |
|---|---|---|
| Fan-out | 一个输入分发到多个 worker | 并行处理 |
| Fan-in | 多个输入合并到一个输出 | 结果聚合 |
| Pipeline | 多阶段串联,每阶段是独立 goroutine | 数据处理流水线 |
| Worker Pool | 固定数量的 worker 消费任务队列 | 限制并发度 |
| Semaphore | 用带缓冲的 channel 限制并发 | 资源访问限流 |
详见并发模式。
Channel vs Mutex 选择依据
| 场景 | Channel | Mutex |
|---|---|---|
| 传递数据的所有权 | ✅ | ❌ |
| 协调多个 goroutine | ✅ | ❌ |
| 保护共享状态 | ❌ | ✅ |
| 简单的计数器/标志 | ❌ | ✅(或 atomic) |
| 生产者-消费者模型 | ✅ | ❌ |
经验法则
- 如果需要传递数据或通知事件→ 用 Channel
- 如果需要保护共享状态(缓存、计数器)→ 用 Mutex
- 如果是简单的原子操作→ 用
sync/atomic
学习建议
推荐学习路径
- Goroutine 与调度器 → 理解 GMP 模型、goroutine 生命周期
- Channel → 有缓冲/无缓冲、关闭语义、select
- sync 包 → Mutex、WaitGroup、Once、Map
- Context → 取消传播、超时控制
- 并发模式 → Pipeline、Worker Pool、Fan-in/Fan-out
- 数据竞争 → race detector、常见竞态