
本文深入剖析 go 语言中多生产者/多消费者并发模型的典型陷阱,重点揭示全局变量 `seq` 的数据竞争问题、无缓冲 channel 的同步机制本质,并提供基于 `atomic` 和结构化 channel 设计的安全实现方案。
在 Go 并发编程中,“多个生产者 + 多个消费者”看似简单的模式,实则极易因隐式同步假象掩盖底层竞态风险。你提供的代码看似稳定输出 1–1000 的有序序列,但这并非线程安全的保证,而是侥幸的巧合——根源在于未受保护的全局变量 seq 引发的数据竞争(data race),以及对无缓冲 channel 行为的误解。
? 为什么看似“有序”?——无缓冲 channel 的隐式串行化
requestChan 和 generatorChan 均为无缓冲 channel(make(chan uint64)),这意味着:
- 每次
- 同样,generatorChan
这种阻塞特性强制了请求与响应的配对时序,但它完全不保证 seq++ 操作的原子性。多个 generateStuff goroutine 可能同时从 requestChan 接收请求后,几乎同时读取、递增、写回 seq —— 这正是经典的“读-改-写”竞态场景。
⚠️ 危险信号:go run -race main.go 会立即报告 Found 1 data race(s),并定位到 seq = seq + 1 行。竞态后果不可预测:重复值、跳变、甚至 panic。
✅ 正确解法:用原子操作替代共享变量递增
消除竞态最直接的方式是避免对 seq 的非原子访问。使用 sync/atomic 包:
import "sync/atomic" // 安全递增(返回新值) s := atomic.AddUint64(&seq, 1) generatorChan <- s
atomic.AddUint64 是 CPU 级原子指令,在任意 goroutine 数量和 GOMAXPROCS 设置下均严格保证递增的完整性与可见性。
? 完整健壮实现(含最佳实践)
以下为优化后的生产级参考实现,已修复竞态、增强可读性并确保资源清理:
package main
import (
"log"
"sync"
"sync/atomic"
)
var seq uint64
var requestChan = make(chan uint64, 10) // 可选:加小缓冲提升吞吐
var generatorChan = make(chan uint64, 10)
func generator(genID int) {
for reqID := range requestChan { // 支持优雅关闭
s := atomic.AddUint64(&seq, 1) // ✅ 原子递增
log.Printf("Gen[%02d] ← Req[%03d] → Seq[%04d]", genID, reqID, s)
generatorChan <- s
}
}
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done()
for i := 0; i < 5; i++ {
requestChan <- uint64(id)
result := <-generatorChan
log.Printf("\t\tWorker[%03d] → Seq[%04d]", id, result)
}
}
func main() {
log.SetFlags(log.Lmicroseconds | log.Lshortfile)
const numGenerators, numWorkers = 20, 200
var wg sync.WaitGroup
// 启动生成器
for i := 0; i < numGenerators; i++ {
go generator(i)
}
// 启动工作者
wg.Add(numWorkers)
for i := 0; i < numWorkers; i++ {
go worker(i, &wg)
}
wg.Wait()
close(requestChan) // 通知所有 generator 退出
}? 关键注意事项
- 永远不要依赖无缓冲 channel 掩盖竞态:它只协调通信时机,不提供内存同步保障;
- GOMAXPROCS=1 会隐藏竞态:单 OS 线程下调度顺序较固定,但切换到多核环境即暴露问题;
- log 比 fmt 更适合并发日志:log 内置锁,避免输出错乱(虽非性能最优,但调试友好);
- 显式关闭 channel:向 requestChan 发送方发送 close(),使 range 循环自然退出,避免 goroutine 泄漏;
- 缓冲区权衡:小缓冲(如 make(chan T, 10))可缓解突发请求压力,但不改变逻辑正确性前提。
通过原子操作+清晰 channel 协议,你将获得真正可扩展、可验证、跨平台一致的多生产者-多消费者系统。记住:并发的确定性来自显式同步,而非偶然的调度顺序。










