文章目录
Go在处理多任务的过程,很容易有通道并发操作,这里讲解一个常见的并发控制流程,通过控制Goroutine的数量,来防止过多的 Goroutine 会导致调度开销过高,甚至耗尽内存或文件描述符。
方案:使用 Goroutine 池: 限制 Goroutine 的最大并发数量
话不多说,先看代码
package main
import (
"fmt"
"sync"
)
func worker(task int, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("Processing task: %d\n", task)
}
func main() {
tasks := make([]int, 1000) // 模拟 1000 个任务
for i := range tasks {
tasks[i] = i
}
const maxWorkers = 10
taskCh := make(chan int, len(tasks))
var wg sync.WaitGroup
// 启动固定数量的 Goroutine
for i := 0; i < maxWorkers; i++ {
go func() {
for task := range taskCh {
worker(task, &wg)
}
}()
}
// 分发任务
for _, task := range tasks {
wg.Add(1)
taskCh <- task
}
close(taskCh)
wg.Wait()
fmt.Println("All tasks completed")
}
1. 总体逻辑
代码实现了一个固定 Goroutine 池来并发处理任务的模型。
- 目标:限制并发 Goroutine 的数量,避免过多 Goroutine 导致调度开销过大。
- 逻辑:
- 将所有任务放入
tasks
列表。 - 创建一个缓冲 Channel (
taskCh
) 来存储任务。 - 创建一个 Goroutine 池,每个 Goroutine 从
taskCh
中取任务并处理。 - 使用
sync.WaitGroup
等待所有任务完成后退出程序。
- 将所有任务放入
2. 关键逻辑解析
2.1 模拟任务列表
tasks := make([]int, 1000) // 模拟 1000 个任务
for i := range tasks {
tasks[i] = i
}
- 功能:创建一个包含 1000 个任务的列表(任务用整数表示)。
- 目的:提供足够的任务,用于模拟大规模任务处理。
2.2 控制最大并发数
const maxWorkers = 10
taskCh := make(chan int, len(tasks))
- 功能:
maxWorkers = 10
:设置最多允许 10 个 Goroutine 并发处理任务。- taskCh:使用缓冲 Channel 存储待处理的任务。
- 缓冲大小设置为
len(tasks)
,确保任务队列不会因缓冲区满而阻塞。
- 缓冲大小设置为
2.3 启动固定数量的 Goroutine
for i := 0; i < maxWorkers; i++ {
go func() {
for task := range taskCh {
worker(task, &wg)
}
}()
}
-
逻辑:
- 启动
maxWorkers
个 Goroutine,每个 Goroutine 持续从taskCh
中取任务处理。 - 每个任务由
worker
函数处理。
- 启动
-
关键点:
-
for task := range taskCh:
- Goroutine 会阻塞在这里,直到从
taskCh
取到任务。 - 当
taskCh
被关闭时,range
自动退出,Goroutine 结束。
- Goroutine 会阻塞在这里,直到从
-
2.4 分发任务
for _, task := range tasks {
wg.Add(1)
taskCh <- task
}
close(taskCh)
- 逻辑:
- 遍历
tasks
,将每个任务放入taskCh
。 - 使用
wg.Add(1)
增加sync.WaitGroup
的计数,表示有一个任务待完成。 - 分发完所有任务后,关闭
taskCh
,通知所有 Goroutine 没有新任务了。
- 遍历
2.5 等待任务完成
wg.Wait()
fmt.Println("All tasks completed")
- 逻辑:
wg.Wait()
阻塞主 Goroutine,直到所有任务完成(即wg.Done()
被调用完)。- 所有任务完成后打印
"All tasks completed"
,程序退出。
3. 核心组件分析
3.1 sync.WaitGroup
的作用
- 功能:
- 用于等待所有任务完成。
- 使用方法:
- 每分发一个任务时调用
wg.Add(1)
。 - 每完成一个任务时调用
wg.Done()
。 - 调用
wg.Wait()
阻塞,直到计数归零。
- 每分发一个任务时调用
3.2 缓冲 Channel 的作用
- 功能:
- 存储任务,控制任务流速,避免 Goroutine 因取不到任务而频繁阻塞。
- 缓冲大小:
- 设置为
len(tasks)
,确保所有任务可以全部放入 Channel 而不阻塞。
- 设置为
3.3 固定 Goroutine 池
- 目的:
- 限制并发数,避免创建过多 Goroutine 导致性能问题。
- 实现:
- 使用一个固定数量的 Goroutine 取任务并处理。
4. 优点与适用场景
优点
- 限制并发数:通过固定 Goroutine 池控制最大并发数。
- 高效资源利用:避免因过多 Goroutine 导致的调度开销。
- 简单灵活:适用于需要批量处理任务的场景。
适用场景
- 大量任务需要并发处理,但任务处理时间相对均衡。
- 高并发场景下需要控制 Goroutine 数量。
以上通过 Goroutine 池的方式,实现了高效的并发任务调度,是 Go 并发编程中的经典模式之一。