《Go语言圣经》并发循环
一、问题背景:图片缩略图生成
我们需要为多个图片文件生成缩略图,每个图片处理是独立的,属于易并行问题(Embarrassingly Parallel)——这类问题适合通过并发提升效率。
二、顺序执行版本:makeThumbnails
func makeThumbnails(filenames []string) {
for _, f := range filenames {
if _, err := thumbnail.ImageFile(f); err != nil {
log.Println(err)
}
}
}
- 特点:逐个处理图片,完全顺序执行,CPU和IO资源未充分利用。
- 缺点:处理时间 = 所有图片处理时间之和,无法利用多核CPU或隐藏IO延迟。
三、初尝并发:makeThumbnails2(错误示范)
func makeThumbnails2(filenames []string) {
for _, f := range filenames {
go thumbnail.ImageFile(f) // 启动goroutine处理图片
}
}
- 问题:
- 主函数启动所有goroutine后立即返回,不等待处理完成
- 无法感知goroutine是否出错或完成
- 可能导致程序提前退出,缩略图未生成完毕
- 关键结论:并发≠并行,需配合同步机制才能正确控制流程。
四、同步机制1:用channel实现完成信号
func makeThumbnails3(filenames []string) {
ch := make(chan struct{}) // 无缓冲channel作为信号通道
for _, f := range filenames {
go func(f string) {
thumbnail.ImageFile(f)
ch <- struct{}{} // 处理完成后发送信号
}(f)
}
// 等待所有信号,确保所有goroutine完成
for range filenames {
<-ch
}
}
-
核心逻辑:
- 每个goroutine完成后向channel发送一个空结构体
- 主函数通过接收len(filenames)次信号来确认所有任务完成
-
问题:
- 阻塞主协程:主协程必须等待所有子协程完成,无法提前返回或处理结果。
- 错误处理缺失:如果某个子协程出错,无法通知主协程。
- 结果收集缺失:无法获取每个子协程的处理结果(如生成的缩略图文件名)。
- 无法提前取消:如果某个子协程执行时间过长,无法中途终止其他子协程。
-
闭包陷阱:
// 错误写法:所有goroutine共享同一个f变量 go func() { thumbnail.ImageFile(f) }()
- 原因:Go的for循环变量在闭包中是引用传递,所有goroutine会使用最后一个f的值
- 解决:通过函数参数显式传递当前f的值(如makeThumbnails3中的做法)
五、同步机制2:用channel返回错误(潜在bug)
func makeThumbnails4(filenames []string) error {
errors := make(chan error) // 无缓冲channel接收错误
for _, f := range filenames {
go func(f string) {
_, err := thumbnail.ImageFile(f)
errors <- err
}(f)
}
for range filenames {
if err := <-errors; err != nil {
return err // 发现错误立即返回,导致channel未排空
}
}
return nil
}
- bug分析:
- 当第一个错误出现时,主函数直接返回,剩余goroutine向errors channel发送数据时会永久阻塞
- 导致goroutine泄露:未结束的goroutine占用资源,可能引发程序卡死
- 解决方案:使用缓冲channel(make(chan error, len(filenames))),允许非阻塞发送
六、优化版本:用缓冲channel返回结果
func makeThumbnails5(filenames []string) (thumbfiles []string, err error) {
type item struct {
thumbfile string
err error
}
ch := make(chan item, len(filenames)) // 缓冲大小等于任务数
for _, f := range filenames {
go func(f string) {
thumbfile, err := thumbnail.ImageFile(f)
ch <- item{thumbfile, err}
}(f)
}
for range filenames {
it := <-ch
if it.err != nil {
return nil, it.err
}
thumbfiles = append(thumbfiles, it.thumbfile)
}
return thumbfiles, nil
}
- 优势:
- 缓冲channel允许所有goroutine无阻塞地发送结果
- 主函数按顺序接收结果,处理错误并收集文件名
- 避免了goroutine泄露问题
七、高级同步:用WaitGroup处理未知任务数
func makeThumbnails6(filenames <-chan string) int64 {
sizes := make(chan int64)
var wg sync.WaitGroup // 任务计数器
for f := range filenames {
wg.Add(1) // 任务数+1
go func(f string) {
defer wg.Done() // 任务完成时-1
thumb, err := thumbnail.ImageFile(f)
if err != nil {
log.Println(err)
return
}
info, _ := os.Stat(thumb)
sizes <- info.Size() // 发送文件大小
}(f)
}
// 独立goroutine等待所有任务完成并关闭channel
go func() {
wg.Wait()
close(sizes)
}()
// 累加所有文件大小
var total int64
for size := range sizes {
total += size
}
return total
}
- WaitGroup核心用法:
wg.Add(n)
:添加n个待完成任务wg.Done()
:标记一个任务完成(等价于Add(-1))wg.Wait()
:阻塞直到计数器归零
- 设计亮点:
- 通过channel接收文件名,适用于任务数未知的场景
- 独立goroutine负责关闭结果channel,避免main goroutine阻塞
defer wg.Done()
确保即使出错也能正确减少计数器
八、性能优化:限制并发数
- 如果处理大量文件,可通过 semaphore 模式 限制同时运行的协程数量:
func makeThumbnails6(filenames []string) error {
tokens := make(chan struct{}, 5) // 最多5个并发
var wg sync.WaitGroup
errCh := make(chan error, 1)
for _, f := range filenames {
wg.Add(1)
go func(f string) {
defer wg.Done()
// 获取令牌
tokens <- struct{}{}
defer func() { <-tokens }() // 释放令牌
if err := thumbnail.ImageFile(f); err != nil {
select {
case errCh <- err:
default:
}
}
}(f)
}
// 等待所有任务完成并检查错误
go func() {
wg.Wait()
close(errCh)
}()
if err, ok := <-errCh; ok {
return err
}
return nil
}
优势:通过 tokens
通道控制并发数,避免系统资源耗尽。