本文详解 go 语言中多生产者-多消费者场景下的典型竞态问题,通过分析原始代码的数据竞争根源,演示如何用原子操作、通道同步与正确关闭机制构建线程安全的并发流水线。
在 Go 的并发模型中,“多个 goroutine 同时修改全局变量”是引发不可预测行为的高发场景。原始代码看似能稳定输出 1–1000 的递增序列,实则掩盖了一个严重问题:对 seq 的非原子读-改-写操作(seq = seq + 1)构成数据竞争(data race)。该竞态未暴露,并非逻辑正确,而是受限于单线程调度(GOMAXPROCS=1)、通道阻塞带来的“伪顺序性”,以及运行环境的偶然性。
? 为什么没看到重复数字?——误解的根源
- requestChan 是无缓冲通道,所有消费者发送请求时会阻塞,直到某个 generateStuff goroutine 从该通道接收。这形成了天然的“请求串行化”假象,但不保护后续的 seq 修改。
- 多个 generateStuff goroutine 可能几乎同时执行
- fmt.Println 非并发安全(尤其在 Playground 等受限环境),其输出交织可能进一步掩盖竞态表现;而 log.Printf 内部加锁,更适合调试并发行为。
✅ 正确实现:消除竞态 + 明确生命周期
核心改进点如下:
- 用 atomic.AddUint64 替代 seq++:保证自增操作的原子性,杜绝计数错误;
- 使用 range 读取通道 + 显式 close():避免 goroutine 永久阻塞,实现优雅退出;
- 移除全局变量依赖,强化通道语义:requestChan 承载“任务分发”,generatorChan 承载“结果返回”,职责清晰;
- 启用竞态检测器验证:go run -race main.go 是开发阶段必做步骤。
以下是重构后的生产就绪示例: