最简单的生产-消费者,你都会遇到哪些问题?
在 Go 语言中,利用 Goroutine 和 Channel 实现"生产者-消费者"模型,几乎是每一个 Gopher 的必修课。
最初模型
基础订单结构
go
type Order struct {
id int64 // 订单id
Amount float64 // 金额
status string // 状态
}
产品id\用户id\创建时间等等,与本次演示无关就写了!
基础生产消费
go
// 生产者
func orderProduct(orderChan chan<- Order, max int) {
for i := 0; i < max; i++ {
order := Order{
id: int64(i),
Amount: rand.Float64(),
status: "pending",
}
orderChan <- order
// 模拟生产
fmt.Printf("生成订单:ID= %d , 金额 = %.2f\n", order.id, order.Amount)
time.Sleep(1 * time.Second)
}
close(orderChan)
}
// 消费者
func orderProcessor(orderChan <-chan Order) {
for order := range orderChan {
// 模拟消费耗时(比生产慢5倍)
fmt.Printf("处理订单:ID= %d , 金额 = %.2f\n", order.id, order.Amount)
time.Sleep(500 * time.Millisecond)
order.status = "done"
}
}
当前模型的痛点问题
channel 关闭不安全,panic 会导致内存泄漏
在生产者函数的最后写了close(orderChan),这看起来没问题,但它有一个致命的漏洞:如果生产者在循环过程中发生 panic,那么close(orderChan)永远不会被执行。
go
func orderProduct(orderChan chan<- Order, max int) {
for i := 0; i < max; i++ {
..................
// 假设这里调用了某个第三方接口,突然发生了panic
someRiskyOperation()
orderChan <- order
..................
}
close(orderChan) // 如果上面发生panic,这行代码永远不会执行!
}
一旦生产者 panic 退出,channel 没有被关闭,所有消费者都会永远阻塞在for range orderChan上,变成僵尸 goroutine。这些 goroutine 会一直占用内存,随着时间推移,内存泄漏会越来越严重,最终导致 OOM。
处理结果石沉大海,无法追踪订单状态
消费者处理完订单后,只是默默地把order.status改成了 "done",但这个结果没有被任何地方收集。
go
func orderProcessor(orderChan <-chan Order) {
for order := range orderChan {
order.status = "done" // 改完就没了!谁也不知道这个订单处理成功了
}
}
在实际业务中,这是完全不可接受的:
- 无法统计成功处理了多少订单,失败了多少
- 无法给用户发送订单处理完成的通知
- 无法将处理结果写入数据库或者其他下游系统
- 出了问题无法排查,不知道哪个订单在哪个环节出了错
defer 安全关闭 channel + 统一结果聚合层。
go
// 订单生产者
func orderProduct(orderChan chan<- Order, max int) {
defer close(orderChan)
for i := 0; i < max; i++ {
order := Order{
id: int64(i),
UserId: fmt.Sprintf("user_%d", i),
Amount: rand.Float64(),
status: "pending",
createTime: time.Now(),
}
orderChan <- order
// 模拟生产耗时
fmt.Printf("生成订单:ID= %d , UserId = %s , 金额 = %.2f\n", order.id, order.UserId, order.Amount)
time.Sleep(1 * time.Second)
}
}
// 订单消费者
func orderProcessor(orderChan <-chan Order, resultChan chan<- Order) {
defer close(resultChan)
for order := range orderChan {
// 模拟处理订单
fmt.Printf("处理订单:ID= %d , UserId = %s , 金额 = %.2f\n", order.id, order.UserId, order.Amount)
time.Sleep(1 * time.Second)
order.status = "done"
resultChan <- order
}
}
// 结果聚合处理器
func orderResultProcessor(resultChan <-chan Order, done chan<- bool) {
for order := range resultChan {
fmt.Printf("处理结果:ID= %d , UserId = %s , 状态 = %s\n", order.id, order.UserId, order.status)
}
done <- true
}
func main() {
// 初始化通道
orderChan := make(chan Order, 10)
resultChan := make(chan Order, 10)
done := make(chan bool)
// 启动生产者
go orderProduct(orderChan, 20)
// 启动4个消费者协程
for i := 0; i < 4; i++ {
go orderProcessor(orderChan, resultChan)
}
// 启动结果聚合协程
go orderResultProcessor(resultChan, done)
// 主线程等待全部处理完成
<-done
fmt.Println("所有订单处理完毕")
}
改进
改用 defer close(orderchan)
使用 defer,无论函数是因为正常执行完毕 、提前 return 还是发生 panic 退出 ,Go 运行时都会确保 close() 被执行。 这极大地减少了消费者协程因为永远等不到关闭信号而导致协程泄漏 或死锁的风险。
添加聚合器 (Aggregator)
1. 避免锁冲突(无锁化状态管理)
假设你的业务需求变了:你需要统计这 20 个订单里,成功了多少个,总金额是多少。
- 如果没有聚合器: 4 个消费者协程都需要去修改同一个"总金额"变量。为了防止并发写入导致数据错乱(数据竞争 Data Race),你必须给这个变量加锁(
sync.Mutex)。一旦加锁,这 4 个消费者实际上就变成了串行排队,并发的性能优势就大打折扣了。 - 有了聚合器: 4 个消费者只需要把结果扔进
resultChan。聚合器作为一个单协程 在默默接收数据,它可以安全地在内部维护一个总金额的变量,累加计算。没有任何锁的开销,极其高效。
2. 优化 I/O 操作(批量入库/网络请求)
处理完订单后,我们通常需要把结果更新到数据库(比如 MySQL 或 Redis)。
- 如果没有聚合器: 4 个消费者各自去连数据库写数据。如果并发量大(比如 1000 个消费者),瞬间会向数据库发起 1000 个连接,很容易把数据库的连接池打爆。
- 有了聚合器: 聚合器可以做一个"攒批"操作(Batching)。它可以等收集到 100 个订单结果,或者每隔 1 秒钟,把收集到的数据打包成一条
INSERT ... VALUES (...)语句,一次性写入数据库。这极大地降低了数据库的压力。
3. 恢复数据的顺序(排序)
并发的特点就是无序。即使生产者按 1 到 20 的顺序发送订单,消费者处理完毕的顺序也是完全随机的。 如果你的系统(比如给前端返回的 API,或者生成报表)要求最终的结果必须按订单 ID 排序怎么办? 聚合器就可以承担这个工作:它把所有零散的结果收集到一个切片(Slice)里,等所有消费者都完工后,对切片进行一次排序,然后再输出最终结果。
致命的隐藏 Bug
问题出在 orderProcessor(消费者)函数中:
Go
func orderProcessor(orderChan <-chan Order, resultChan chan<- Order) {
defer close(resultChan) // ❌ 这里会引发 Panic
// ...
}
main 函数中启动了 4个 orderProcessor 协程。当 orderChan 里的数据被处理完后,这 4 个协程都会退出,这意味着:
- 重复关闭通道: 第一个退出的协程关闭了
resultChan。当第二个协程尝试再次关闭它时,Go 会抛出panic: close of closed channel。 - 向已关闭的通道发送数据: 如果某一个消费者协程跑得快,提前退出了并关闭了
resultChan,其他 3 个还在干活的协程尝试执行resultChan <- order时,会抛出panic: send on closed channel。
🛠️ 解决方案:使用 sync.WaitGroup
为了解决多协程的同步问题,Go 的标准做法是使用 sync.WaitGroup。我们需要:
- 让每个消费者完成工作后通知
WaitGroup。 - 启动一个独立的后台协程,等待所有消费者都干完活(
wg.Wait()),然后再统一关闭resultChan。
最终版完整代码
go
type Order struct {
id int64
Amount float64
status string
}
// 生产者
func orderProduct(orderChan chan<- Order, max int) {
defer close(orderChan)
for i := 0; i < max; i++ {
order := Order{
id: int64(i),
Amount: rand.Float64(),
status: "pending",
}
orderChan <- order
// 模拟生产
fmt.Printf("生成订单:ID= %d , 金额 = %.2f\n", order.id, order.Amount)
time.Sleep(1 * time.Second)
}
}
// 消费者
func orderProcessor(orderChan <-chan Order, resultChan chan<- Order, wg *sync.WaitGroup) {
defer wg.Done() // 协程退出时,通知 WaitGroup 任务完成
for order := range orderChan {
// 模拟消费
fmt.Printf("处理订单:ID= %d , 金额 = %.2f\n", order.id, order.Amount)
time.Sleep(1 * time.Second)
order.status = "done"
resultChan <- order
}
}
// 统一聚合
func orderResultProcessor(resultChan <-chan Order, done chan<- bool) {
for order := range resultChan {
fmt.Printf("处理结果:ID= %d , 状态 = %s\n", order.id, order.status)
}
done <- true
}
func main() {
orderChan := make(chan Order, 10)
resultChan := make(chan Order, 10)
done := make(chan bool)
// 1. 启动生产者
go orderProduct(orderChan, 20)
// 2. 启动消费者池
var wg sync.WaitGroup
for i := 0; i < 4; i++ {
wg.Add(1) // 每启动一个消费者,计数器加 1
go orderProcessor(orderChan, resultChan, &wg)
}
// 3. 启动协调者:等待所有消费者结束,然后安全关闭 resultChan
go func() {
wg.Wait() // 阻塞等待,直到计数器归零
close(resultChan) // 此时安全关闭
}()
// 4.启动聚合器
go orderResultProcessor(resultChan, done)
// 5.阻塞主协程,等待聚合器完成
<-done
}
❗不知道以上代码还有什么问题欢迎来补充!(^o^)/~