《Go语言圣经》并发循环

《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处理图片
    }
}
  • 问题
    1. 主函数启动所有goroutine后立即返回,不等待处理完成
    2. 无法感知goroutine是否出错或完成
    3. 可能导致程序提前退出,缩略图未生成完毕
  • 关键结论:并发≠并行,需配合同步机制才能正确控制流程。
四、同步机制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
    }
}
  • 核心逻辑

    1. 每个goroutine完成后向channel发送一个空结构体
    2. 主函数通过接收len(filenames)次信号来确认所有任务完成
  • 问题

    1. 阻塞主协程:主协程必须等待所有子协程完成,无法提前返回或处理结果。
    2. 错误处理缺失:如果某个子协程出错,无法通知主协程。
    3. 结果收集缺失:无法获取每个子协程的处理结果(如生成的缩略图文件名)。
    4. 无法提前取消:如果某个子协程执行时间过长,无法中途终止其他子协程。
  • 闭包陷阱

    // 错误写法:所有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分析
    1. 当第一个错误出现时,主函数直接返回,剩余goroutine向errors channel发送数据时会永久阻塞
    2. 导致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
}
  • 优势
    1. 缓冲channel允许所有goroutine无阻塞地发送结果
    2. 主函数按顺序接收结果,处理错误并收集文件名
    3. 避免了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():阻塞直到计数器归零
  • 设计亮点
    1. 通过channel接收文件名,适用于任务数未知的场景
    2. 独立goroutine负责关闭结果channel,避免main goroutine阻塞
    3. 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 通道控制并发数,避免系统资源耗尽。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值