Files
cs-note/hzh/GolangStar/Go语言进阶/协程池.md
T

198 lines
4.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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["任务队列<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
```
### 核心组件
| 角色 | 说明 |
|------|------|
| 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]]