4. 带缓冲 channel 的生产者-消费者模型
带缓冲 channel 的生产者-消费者模型
带缓冲 channel 的核心价值在于解耦生产与消费的节奏:生产者不必等待消费者就绪即可连续投递,消费者也不必与生产者严格同步。缓冲容量 cap 决定了系统能容忍多大的瞬时速率差。当缓冲区写满时,生产者的 ch <- task 会阻塞,直到消费者取走一个元素腾出空位。
下面实现一个可观察的模型:生产者每 1 秒生成一个任务,消费者以可调的处理耗时并发取出处理。通过打印时间戳与缓冲区长度,可以直观看到缓冲填满、生产者阻塞、以及消费者加速后吞吐量回升的全过程。
package main
import (
"fmt"
"sync"
"time"
)
type Task struct {
ID int
}
func main() {
const (
bufSize = 3 // 缓冲区容量
taskCount = 8 // 总任务数
produceEvery = 1 * time.Second // 生产者节奏
consumeCost = 2 * time.Second // 单个消费者处理耗时
consumers = 2 // 消费者数量
)
ch := make(chan Task, bufSize)
var wg sync.WaitGroup
// 生产者:每秒投递一个任务
wg.Add(1)
go func() {
defer wg.Done()
defer close(ch)
for i := 1; i <= taskCount; i++ {
t := Task{ID: i}
start := time.Now()
ch <- t // 缓冲满时在此阻塞
wait := time.Since(start)
fmt.Printf("[%s] 生产 Task-%d len=%d/%d 阻塞=%v\n",
time.Now().Format("15:04:05"), t.ID, len(ch), cap(ch), wait.Round(time.Millisecond))
time.Sleep(produceEvery)
}
}()
// 消费者:并发取出并处理
for c := 1; c <= consumers; c++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for t := range ch {
fmt.Printf("[%s] 消费者%d 取出 Task-%d len=%d/%d\n",
time.Now().Format("15:04:05"), id, t.ID, len(ch), cap(ch))
time.Sleep(consumeCost)
}
}(c)
}
wg.Wait()
fmt.Println("全部任务处理完成")
}
运行后重点观察三件事:
- 缓冲区填充阶段:前 3 个任务几乎瞬间写入,
len从 0 涨到 3,生产者无阻塞。 - 阻塞出现:第 4 个任务开始,
len=3/3已满,生产者打印的阻塞值不再为 0,说明它被挂起等待消费者腾位。 - 吞吐量变化:2 个消费者并行处理,单个耗时 2 秒,理论消费速率约 1 个/秒,与生产速率持平;若把
consumeCost调大或consumers调小,阻塞时间会显著增长。
把
bufSize 改为 0(无缓冲)再运行,可对比:生产者每次投递都必须等到消费者接手,阻塞几乎立即发生,这就是无缓冲 channel 的同步语义。关键参数与观察指标
| 参数 | 含义 | 调整后的影响 |
|---|---|---|
bufSize | 缓冲区容量 | 越大,生产者越不易阻塞,但内存占用与延迟增加 |
produceEvery | 生产间隔 | 越小,生产越快,缓冲区越快填满 |
consumeCost | 单个任务处理耗时 | 越大,消费越慢,阻塞越频繁 |
consumers | 消费者数量 | 越多,消费吞吐越高,阻塞减少 |
当
consumers 过少或 consumeCost 过大时,生产者会长时间阻塞在 ch <- t 上。此时程序不会死锁(因为消费者仍在工作),但整体吞吐量受限于消费端,需要增加消费者或降低处理耗时。通过修改上述参数并观察 len 与阻塞时间的变化,可以建立对带缓冲 channel 容量规划的基本直觉:缓冲不是越大越好,它只是吸收速率波动的弹性空间,真正的吞吐上限由消费端决定。