golang批量下载图片并打包zip的性能优化
所以你应该知道,在zip文件里面创建文件是串行的,这是由它的特性决定的。
// Create adds a file to the zip file using the provided name.
// It returns a Writer to which the file contents should be written.
// The file contents will be compressed using the Deflate method.
// The name must be a relative path: it must not start with a drive
// letter (e.g. C:) or leading slash, and only forward slashes are
// allowed. To create a directory instead of a file, add a trailing
// slash to the name.
// The file's contents must be written to the io.Writer before the next
// call to Create, CreateHeader, or Close.
func (w *Writer) Create(name string) (io.Writer, error) {
header := &FileHeader{
Name: name,
Method: Deflate,
}
return w.CreateHeader(header)
}
那么可以优化的空间就在于获取网络图片的时候了,通过多协程的方式来提高效率。
常规的方式是:
// 创建zip文件
zipwriter := zip.NewWriter(file)
// 遍历图片数组
for _, f := range fs {
// 创建文件
// 获取图片内容
// 将内容写入文件
}
70张图片,耗时在 31.62s
优化方案,采用任务流式处理:
遍历图片 --> TaskA --> TaskB
TaskA:多协程,获取图片内容。
TaskB:单协程,写入到zip。
示例代码:
func download() {
type photo struct {
Url string
Name string
Content []byte
}
photoInChan := make(chan photo, 100) // 太小了会有一定的阻塞
photoOutChan := make(chan photo, 100) // 太小了会有一定的阻塞
photoNum := 0 // 总的图片数
photoOver := 0 // 总消费数
isOver := false // 图片是否已遍历完,流式模式下不能只判断 photoNum == photoOver
mu := &sync.Mutex{} // 自增操作存在并发问题,需要加锁
// 获取图片内容,做好协程退出机制
for i := 0; i < 20; i++ {
go func(no int) {
over := false
for {
if over {
break
}
select {
case p := <-photoInChan:
p.Content = util.GetRemoteContent(p.Url)
photoOutChan <- p
mu.Lock()
photoOver++
mu.Unlock()
case <-time.After(time.Millisecond * 500):
// 最后一个任务只会被一个协程消费,其他协程仍然处于阻塞状态,因此需要一个超时处理来实现主动退出。
// 本来想使用context来做,发现并不适合这个场景。
if photoOver == photoNum && isOver {
fmt.Println("download goroutine is returned: ", no)
over = true
}
}
}
}(i)
}
zipfile := "xxx.zip"
f, err := os.Create(zipfile)
if err != nil {
return
}
defer f.Close()
zipwriter := zip.NewWriter(f)
defer zipwriter.Close()
var wg sync.WaitGroup
wg.Add(1)
// 写入zip操作,串行处理
go func() {
defer func() {
wg.Done()
}()
over := 0
for p := range photoOutChan {
iowriter, err := zipwriter.Create(p.Name)
if err != nil {
continue
}
iowriter.Write(p.Content)
over++
if over == photoNum && isOver {
break
}
}
}()
// 图片数组
for _, val := range photos {
arr := strings.Split(val, ".")
ext := arr[len(arr)-1]
photoInChan<-photo{
Url: val,
Name: util.CreatePhotoName() + "." + ext,
}
photoNum++
}
fmt.Println("总图片数:", photoNum)
isOver = true
wg.Wait()
return
}
耗时 20.93s
总体来看耗时降低了 30%,还可以吧。