搜索
简帛阁>技术文章>go高并发时append方法偶现错误解决分析

go高并发时append方法偶现错误解决分析

背景

在实现图片转码的需求时,需要支持最大 500 个图片下载后转换格式;

如果是一个一个下载后转码,耗时太长,需要使用 goroutine 实现 500 个图片并发下载后,并发转码;

但自测过程中发现,会偶现下载后只转换了 499 个图片或更少的情况(全部下载、转码成功的条件下);

然后就开始了打印日志找 bug 的过程。

排查问题

因为并发时使用到了 sync 等待全部协程结束,起初以为是 sync 异步等待出了问题;

打印日志发现,正常执行了 500 次下载,执行完成下载之后,继续执行的转码操作,排除 sync 异步等待有问题;

代码如下:

import (
	"github.com/satori/go.uuid"
	"sync"
)
func downloadFiles(nWait *sync.WaitGroup, urls []interface{}, successFiles *[]string, failedFiles *[]string) {
	// 遍历 urls 进行下载
	for _, value := range urls {
		go func(value interface{}) {
			defer nWait.Done()                                                     // 执行结束,协程减 1
			fullname := config.TranscodeDownloadPath + "/" + uuid.NewV4().String() // 需要确保文件名的唯一性 (防止不同用户同一时间操作了同一文件,导致转码失败)
			err := utils.DownloadCeph(value.(string), fullname)                    // 下载文件
			// 下载文件状态记录
			if err != nil {
				*failedFiles = append(*failedFiles, fullname)
			} else {
				*successFiles = append(*successFiles, fullname)
			}
		}(value)
	}
}
// 前端传入的图片 url
strUrlList := req["strUrlList"]
// 初始化变量
nWait := sync.WaitGroup{}          // 多协程异步等待
var successFiles []string  // 下载成功文件
var failedFiles []string           // 下载失败文件
// 遍历 strUrlList 进行下载
log.Error("开始下载!长度:", len(strUrlList))
nWait.Add(len(strUrlList)) // 等待协程数
downloadFiles(&nWait, strUrlList, &successFiles, &failedFiles)
nWait.Wait() // 阻塞,等待完成
log.Error("下载结束!长度:", len(successFiles))
//...
log.Error("下载转码!")
//...

日志如下:

2022-10-29 21:28:51.996 ERROR   services/tools.go:149   开始下载!长度:500
2022-10-29 21:28:52.486 ERROR   services/tools.go:153   下载结束!长度:499
2022-10-29 21:28:52.486 ERROR   services/tools.go:155   开始转码!

打印更详细的日志,对 for range 循环内的逻辑进行排查;
在单个 for 循环结束时增加日志:

log.Error("下载协程结束: ", len(*successFiles))

发现一处特殊的日志:

2022-10-29 21:40:38.407 ERROR   services/tools.go:35    下载协程结束: 63
2022-10-29 21:40:38.407 ERROR   services/tools.go:35    下载协程结束: 64
2022-10-29 21:40:38.407 ERROR   services/tools.go:35    下载协程结束: 65
2022-10-29 21:40:38.407 ERROR   services/tools.go:35    下载协程结束: 65
2022-10-29 21:40:38.408 ERROR   services/tools.go:35    下载协程结束: 66
2022-10-29 21:40:38.408 ERROR   services/tools.go:35    下载协程结束: 67

两次长度都是 65,切片长度没有发生变化,同一时间点执行两次切片 append 方法,会偶现一次失效,问题原因找到;

解决问题

使用切片索引进行赋值,不再使用 append ;

修复代码如下:

import (
	"github.com/satori/go.uuid"
	"sync"
)
func downloadFiles(nWait *sync.WaitGroup, urls []interface{}, successFiles *[]string, failedFiles *[]string) {
	// 遍历 urls 进行下载
	for index, value := range urls {
		go func(index int, value interface{}) {
			defer nWait.Done()                                                     // 执行结束,协程减 1
			fullname := config.TranscodeDownloadPath + "/" + uuid.NewV4().String() // 需要确保文件名的唯一性 (防止不同用户同一时间操作了同一文件,导致转码失败)
			err := utils.DownloadCeph(value.(string), fullname)                    // 下载文件
			// 下载文件状态记录
			if err != nil {
				(*failedFiles)[index] = fullname
			} else {
				(*successFiles)[index] = fullname
			}
		}(index, value)
	}
}
// 前端传入的图片 url
strUrlList := req["strUrlList"]
// 初始化变量
nWait := sync.WaitGroup{}                                        // 多协程异步等待
successFiles := make([]string, len(strUrlList), len(strUrlList)) // 下载成功文件
failedFiles := make([]string, len(strUrlList), len(strUrlList))  // 下载失败文件
// 遍历 strUrlList 进行下载
nWait.Add(len(strUrlList)) // 等待协程数
downloadFiles(&nWait, strUrlList, &successFiles, &failedFiles)
nWait.Wait() // 阻塞,等待完成
录背景排查问题解决问题背景在实现图片转码的需求,需要支持最大500个图片下载后转换格式;如果是一个一个下载后转码,耗时太长,需要使用goroutine实现500个图片并发下载后,并发转码;但自测过
目录并发下restTemplate的错误分析1问题现象和分析2问题解决使用restTemplate出现的异常并发下restTemplate的错误分析1问题现象和分析orgapachehttpcon
本文实例分析Go语言中append函数用法。分享给大家供大家参考。具体如下:Go语言中append的功能十分强大,使用它可以使很多功能的实现变得更加简洁。以下为简单对比:将一个slice插入到另一个
本决解办法只有一个:队列,下面说的没有用的对于我们开发的网站,如果网站的访问量非常大的话,那么我们就需要考虑相关的并发访问问题了。而并发问题是绝大部分的程序员头疼的问题,但话又说回来了,既然逃避不掉
读写并发的时候复杂的数据库+缓存双写不一致的场景复杂的数据库+缓存双写一致保障方案机器级别的请求路由问题最初级的数据库+缓存双写不一致问题最初级的数据库+缓存双写不一致问题的解决方案实时性比较的数
预备:为下文能表述清晰,预定义如下:1、对一个keyvalue对象[对关系数据库则为pkelse],key称为变量,value称为值。2、对下文提到的变量更新,主要指变量的变更按:insert[首次]
引言在Go语言中,我们通常会用到panic和recover来抛出错误和捕获错误,这一对操作在单协程环境下我们正常用就好了,并不会踩到什么坑。但是在多协程并发环境下,我们常常会碰到以下两个问题。假设我
1、任何的并发,请求总是会有一个顺序的2、java的队列的数据结构是先进先出的取值顺序3、BlockingQueue类(线程安全)(使用方法可以百度)一般使用LinkedBlockingQueue
一、缓存式的Web应用程序架构:在Web层和db层之间加一层cache层,主要目的:减少数据库读取负担,提高数据读取速度。cache存取的媒介是内存,可以考虑采用分布式的cache层,这样更容易破除内
队列+多线程+couchbase缓存,解决并发问题。usingSystem;usingSystemCollectionsGeneric;usingSystemLinq;usingSystemWeb