--- tags: [go, golang, 协程池, 并发, WorkerPool] create time: 2026-06-07 15:00 --- # 协程池 ## 概述 虽然 Go 可以轻松创建数十万个 goroutine,但无限制地创建反而会导致调度开销和 GC 压力。协程池通过将活跃 goroutine 数量控制在合理范围内,实现性能与资源的平衡。 ## 正文 ### 为什么需要协程池? > [!question] 💭 思考 > Go 说可以开 10 万个 goroutine,那是不是意味着我应该每次都 `go func()`? 理论上可以,但实际上: - **过多 goroutine** → GMP 调度器负担加重、CPU 上下文切换频繁 - **过多 goroutine** → GC 扫描对象增多、STW 时间变长 - **过多 goroutine** → 系统内存压力增大 协程池的核心思想:**控制并发度,而非消除并发**。 ```mermaid flowchart TD A["任务队列
Job Channel"] -->|"取任务"| B["Worker 1"] A -->|"取任务"| C["Worker 2"] A -->|"取任务"| D["Worker N"] E["AddTask
提交任务"] --> A subgraph Pool["协程池 (固定 N 个 worker)"] B C D end style A fill:#fff3e0 style E fill:#e8f5e9 style Pool fill:#e3f2fd ``` ### 核心组件 | 角色 | 说明 | |------|------| | Task | 封装待执行的业务逻辑(函数 + 参数) | | Worker | 固定的 goroutine,从任务队列循环取任务执行 | | Pool | 管理 Worker 数量和任务队列的容器 | ### 最小实现 ```go package main import ( "fmt" "sync" "sync/atomic" ) // Task 封装一个可执行的任务 type Task struct { f func() error } func NewTask(f func() error) *Task { return &Task{f: f} } // Pool 协程池 type Pool struct { capacity int // worker 数量 taskQueue chan *Task // 任务缓冲队列 wg sync.WaitGroup // 等待所有 worker 结束 running atomic.Int64 // 当前运行数 } func NewPool(capacity int, queueSize int) *Pool { return &Pool{ capacity: capacity, taskQueue: make(chan *Task, queueSize), } } // Start 启动 pool func (p *Pool) Start() { for i := 0; i < p.capacity; i++ { p.wg.Add(1) go p.worker(i) } } func (p *Pool) worker(id int) { defer p.wg.Done() for task := range p.taskQueue { if task != nil && task.f != nil { task.f() } } } // Submit 提交任务 func (p *Pool) Submit(task *Task) bool { select { case p.taskQueue <- task: return true default: return false // 队列已满,非阻塞拒绝 } } // Stop 停止 pool,关闭任务队列等待所有 worker 完成 func (p *Pool) Stop() { close(p.taskQueue) p.wg.Wait() } ``` 使用示例: ```go func main() { pool := NewPool(3, 10) // 3 个 worker,10 个任务缓冲 pool.Start() for i := 0; i < 20; i++ { pool.Submit(NewTask(func() error { fmt.Printf("处理任务 %d\n", i) return nil })) } pool.Stop() // 优雅关闭 } ``` ### 队列满了怎么办? > [!question] 💭 思考 > 当任务生产速度超过消费速度,且队列也满了,你的协程池应该如何应对? 三种策略各有取舍: | 策略 | 实现方式 | 优点 | 缺点 | |------|---------|------|------| | **阻塞等待** | `p.taskQueue <- task` | 不丢任务 | 生产者可能被卡住 | | **非阻塞拒绝** | `select + default` | 响应快 | 可能丢任务 | | **动态扩缩容** | 监控队列长度增减 worker | 自适应负载 | 实现复杂 | > [!tip] 💡 推荐方案 > 大多数场景下,"有界队列 + 拒绝策略"是最实用的组合。拒绝时可以选择丢弃、记录日志告警,或降级处理。 ### 优雅关闭 > [!warning] ⚠️ 协程池关闭的关键点 > 关闭时必须确保:1) 不再接收新任务;2) 已有任务全部执行完毕;3) 所有 worker goroutine 都退出。 ```go // 正确关闭流程 func (p *Pool) Shutdown() { close(p.taskQueue) // 1. 关闭队列——worker 收到 range 退出信号 p.wg.Wait() // 2. 等待所有 worker 完成剩余任务 } ``` > [!note] 📝 不要直接调用 runtime.Goexit() 或 panic 来终止 goroutine > 这会导致 defer 链不完整、资源泄漏等问题。始终用 channel close + WaitGroup 的方式优雅退出。 ### 实战建议 > [!tip] 💡 协程池调参指南 > - **CPU 密集型**:worker 数量 ≈ CPU 核心数 > - **IO 密集型**:worker 数量 = CPU 核心数 × (1 + 等待时间/计算时间),通常 10~100 倍 > - **混合场景**:根据压测结果调整,观察 CPU 利用率和延迟 P99 > [!warning] ⚠️ 避免在 worker 中 panic > 单个 worker panic 会导致整个程序崩溃。应在 worker 中使用 recover: > ```go > func (p *Pool) worker(id int) { > defer func() { > if r := recover(); r != nil { > log.Printf("worker %d recovered from panic: %v", id, r) > } > p.wg.Done() > }() > for task := range p.taskQueue { ... } > } > ``` ## 关联笔记 - [[hzh/GolangStar/Go语言进阶/Goroutine]] - [[hzh/GolangStar/Go语言进阶/Sync]] - [[hzh/GolangStar/Go语言进阶/Channel]]