4. 带缓冲 channel 的生产者-消费者模型

带缓冲 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 的同步语义。
生产者 每 1s 生成 Task-1..8 带缓冲 channel cap=3 槽1 槽2 槽3 len=3/3 时生产者阻塞 ch <- task 消费者 1 处理 2s 消费者 2 处理 2s range ch range ch 缓冲满 生产者挂起等待 t=0s t=3s t=6s t=9s t=12s

关键参数与观察指标

参数含义调整后的影响
bufSize缓冲区容量越大,生产者越不易阻塞,但内存占用与延迟增加
produceEvery生产间隔越小,生产越快,缓冲区越快填满
consumeCost单个任务处理耗时越大,消费越慢,阻塞越频繁
consumers消费者数量越多,消费吞吐越高,阻塞减少
当 consumers 过少或 consumeCost 过大时,生产者会长时间阻塞在 ch <- t 上。此时程序不会死锁(因为消费者仍在工作),但整体吞吐量受限于消费端,需要增加消费者或降低处理耗时。

通过修改上述参数并观察 len 与阻塞时间的变化,可以建立对带缓冲 channel 容量规划的基本直觉:缓冲不是越大越好,它只是吸收速率波动的弹性空间,真正的吞吐上限由消费端决定。