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

4.9 KiB
Raw Blame History

tags, create time
tags create time
go
golang
协程池
并发
WorkerPool
2026-06-07 15:00

协程池

概述

虽然 Go 可以轻松创建数十万个 goroutine,但无限制地创建反而会导致调度开销和 GC 压力。协程池通过将活跃 goroutine 数量控制在合理范围内,实现性能与资源的平衡。

正文

为什么需要协程池?

[!question] 💭 思考 Go 说可以开 10 万个 goroutine,那是不是意味着我应该每次都 go func()?

理论上可以,但实际上:

  • 过多 goroutine → GMP 调度器负担加重、CPU 上下文切换频繁
  • 过多 goroutine → GC 扫描对象增多、STW 时间变长
  • 过多 goroutine → 系统内存压力增大

协程池的核心思想:控制并发度,而非消除并发。

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 数量和任务队列的容器

最小实现

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()
}

使用示例:

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 都退出。

// 正确关闭流程
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:

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 { ... }
}

关联笔记