2026-06-07 11:08:10 +08:00
|
|
|
|
---
|
2026-06-07 12:14:39 +08:00
|
|
|
|
tags: [go, golang, 协程池, 并发, WorkerPool]
|
|
|
|
|
|
create time: 2026-06-07 15:00
|
2026-06-07 11:08:10 +08:00
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
# 协程池
|
|
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
## 概述
|
|
|
|
|
|
|
|
|
|
|
|
虽然 Go 可以轻松创建数十万个 goroutine,但无限制地创建反而会导致调度开销和 GC 压力。协程池通过将活跃 goroutine 数量控制在合理范围内,实现性能与资源的平衡。
|
|
|
|
|
|
|
|
|
|
|
|
## 正文
|
|
|
|
|
|
|
|
|
|
|
|
### 为什么需要协程池?
|
|
|
|
|
|
|
|
|
|
|
|
> [!question] 💭 思考
|
|
|
|
|
|
> Go 说可以开 10 万个 goroutine,那是不是意味着我应该每次都 `go func()`?
|
|
|
|
|
|
|
|
|
|
|
|
理论上可以,但实际上:
|
|
|
|
|
|
- **过多 goroutine** → GMP 调度器负担加重、CPU 上下文切换频繁
|
|
|
|
|
|
- **过多 goroutine** → GC 扫描对象增多、STW 时间变长
|
|
|
|
|
|
- **过多 goroutine** → 系统内存压力增大
|
|
|
|
|
|
|
|
|
|
|
|
协程池的核心思想:**控制并发度,而非消除并发**。
|
|
|
|
|
|
|
|
|
|
|
|
```mermaid
|
|
|
|
|
|
flowchart TD
|
|
|
|
|
|
A["任务队列<br/>Job Channel"] -->|"取任务"| B["Worker 1"]
|
|
|
|
|
|
A -->|"取任务"| C["Worker 2"]
|
|
|
|
|
|
A -->|"取任务"| D["Worker N"]
|
|
|
|
|
|
|
|
|
|
|
|
E["AddTask<br/>提交任务"] --> A
|
|
|
|
|
|
|
|
|
|
|
|
subgraph Pool["协程池 (固定 N 个 worker)"]
|
|
|
|
|
|
B
|
|
|
|
|
|
C
|
|
|
|
|
|
D
|
|
|
|
|
|
end
|
|
|
|
|
|
|
|
|
|
|
|
style A fill:#fff3e0
|
|
|
|
|
|
style E fill:#e8f5e9
|
|
|
|
|
|
style Pool fill:#e3f2fd
|
2026-06-07 11:08:10 +08:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
### 核心组件
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
| 角色 | 说明 |
|
|
|
|
|
|
|------|------|
|
|
|
|
|
|
| Task | 封装待执行的业务逻辑(函数 + 参数) |
|
|
|
|
|
|
| Worker | 固定的 goroutine,从任务队列循环取任务执行 |
|
|
|
|
|
|
| Pool | 管理 Worker 数量和任务队列的容器 |
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
### 最小实现
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
|
|
|
|
|
```go
|
|
|
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
2026-06-07 12:14:39 +08:00
|
|
|
|
"fmt"
|
|
|
|
|
|
"sync"
|
|
|
|
|
|
"sync/atomic"
|
2026-06-07 11:08:10 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
// Task 封装一个可执行的任务
|
2026-06-07 11:08:10 +08:00
|
|
|
|
type Task struct {
|
2026-06-07 12:14:39 +08:00
|
|
|
|
f func() error
|
2026-06-07 11:08:10 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
func NewTask(f func() error) *Task {
|
|
|
|
|
|
return &Task{f: f}
|
2026-06-07 11:08:10 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
// Pool 协程池
|
2026-06-07 11:08:10 +08:00
|
|
|
|
type Pool struct {
|
2026-06-07 12:14:39 +08:00
|
|
|
|
capacity int // worker 数量
|
|
|
|
|
|
taskQueue chan *Task // 任务缓冲队列
|
|
|
|
|
|
wg sync.WaitGroup // 等待所有 worker 结束
|
|
|
|
|
|
running atomic.Int64 // 当前运行数
|
2026-06-07 11:08:10 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
func NewPool(capacity int, queueSize int) *Pool {
|
|
|
|
|
|
return &Pool{
|
|
|
|
|
|
capacity: capacity,
|
|
|
|
|
|
taskQueue: make(chan *Task, queueSize),
|
|
|
|
|
|
}
|
2026-06-07 11:08:10 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
// Start 启动 pool
|
|
|
|
|
|
func (p *Pool) Start() {
|
|
|
|
|
|
for i := 0; i < p.capacity; i++ {
|
|
|
|
|
|
p.wg.Add(1)
|
|
|
|
|
|
go p.worker(i)
|
|
|
|
|
|
}
|
2026-06-07 11:08:10 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
func (p *Pool) worker(id int) {
|
|
|
|
|
|
defer p.wg.Done()
|
|
|
|
|
|
for task := range p.taskQueue {
|
|
|
|
|
|
if task != nil && task.f != nil {
|
|
|
|
|
|
task.f()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-06-07 11:08:10 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
// Submit 提交任务
|
|
|
|
|
|
func (p *Pool) Submit(task *Task) bool {
|
|
|
|
|
|
select {
|
|
|
|
|
|
case p.taskQueue <- task:
|
|
|
|
|
|
return true
|
|
|
|
|
|
default:
|
|
|
|
|
|
return false // 队列已满,非阻塞拒绝
|
|
|
|
|
|
}
|
2026-06-07 11:08:10 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
// Stop 停止 pool,关闭任务队列等待所有 worker 完成
|
|
|
|
|
|
func (p *Pool) Stop() {
|
|
|
|
|
|
close(p.taskQueue)
|
|
|
|
|
|
p.wg.Wait()
|
2026-06-07 11:08:10 +08:00
|
|
|
|
}
|
2026-06-07 12:14:39 +08:00
|
|
|
|
```
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
使用示例:
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
```go
|
|
|
|
|
|
func main() {
|
|
|
|
|
|
pool := NewPool(3, 10) // 3 个 worker,10 个任务缓冲
|
|
|
|
|
|
pool.Start()
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
for i := 0; i < 20; i++ {
|
|
|
|
|
|
pool.Submit(NewTask(func() error {
|
|
|
|
|
|
fmt.Printf("处理任务 %d\n", i)
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}))
|
|
|
|
|
|
}
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
pool.Stop() // 优雅关闭
|
2026-06-07 11:08:10 +08:00
|
|
|
|
}
|
2026-06-07 12:14:39 +08:00
|
|
|
|
```
|
2026-06-07 11:08:10 +08:00
|
|
|
|
|
2026-06-07 12:14:39 +08:00
|
|
|
|
### 队列满了怎么办?
|
|
|
|
|
|
|
|
|
|
|
|
> [!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 完成剩余任务
|
2026-06-07 11:08:10 +08:00
|
|
|
|
}
|
|
|
|
|
|
```
|
2026-06-07 12:14:39 +08:00
|
|
|
|
|
|
|
|
|
|
> [!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]]
|