为什么要用并发?
这个疑问产生在如下情境:某次,调用某个接口时,我发现千辛万苦写完的接口,它返回的特别慢,居然要 7/8 s!
整个人都不好了,窘迫,不解。
于是尝试将代码重构,删掉多余的逻辑,发现几乎没有什么变化。于是通过中断代码调试来测验程序运行耗时的分布状况,发现时间都耗在了循环请求外部资源这个模块,反而复杂的逻辑处理并没有占据多少资源。于是,想起学习golang时用的教科书:
前言 · Go语言圣经books.studygolang.com其中,第八章讲到了Goroutines,也就是并发设计。这意味着我可以在某些可以为某些代码模块添加并发设计,减少程序耗时。
如何使用并发设计?
根据这段时间的摸索,我发现在项目中goroutines的应用场景大多是两个及以上不相关的代码模块之间。如:
var tmp []int = [1,...,10000000]
for _, v := range tmp {
go func(v int) {
//todo
}(v)
}
上述代码是golang并发设计的粗略展示,在大多数情况下,goroutine内的todo要相对复杂,因为goroutines即使很快启动,也是需要一定时间,倘若一次并发启动的时间大于goroutines实际起作用的时间,那么这个并发设计不仅不能做到提高程序速率,相反还会降低。
为了证实我的结论,我通过一个简单的例子来阐述:
- 容量为1000000的数组,无并发求和,求程序运行时间;
func Test(){
_stamp := time.Now().UnixNano()
tmp := []int{}
for i := 0; i < 10000000; i++ {
tmp = append(tmp, i)
}
sum := 0
for _, v := range tmp {
sum += v
}
stamp_ := time.Now().UnixNano()
c.JSON(http.StatusOK, gin.H{
"status": "success",
"result": sum,
"ms": (stamp_ - _stamp) / 1000000,
})
}
结果如下,耗时5ms。
{
"ms": 5,
"result": 49999995000000,
"status": "success"
}
- 容量为1000000的数组,并发求和,求程序运行时间;
func AgentsPing(c *gin.Context) {
sum := 0
tmp := []int{}
for i := 0; i < 10000000; i++ {
tmp = append(tmp, i)
}
finish := make(chan int)
var wg sync.WaitGroup
wg.Add(10000000)
_stamp := time.Now().UnixNano()
for _, v := range tmp {
go func(v int) {
sum += v
}(v)
defer wg.Done()
}
go func() {
wg.Wait()
finish <- 1
}()
stamp_ := time.Now().UnixNano()
c.JSON(http.StatusOK, gin.H{
"status": "success",
"result": sum,
"ms": (stamp_ - _stamp) / 1000000,
})
}
结果如下:耗时5947ms。
{
"ms": 5947,
"result": 49999995000000,
"status": "success"
}
小结
综上所述,并发设计需慎重,并不是所有场合都适合并发,因为启动并发与并发作用时间并不明确。在项目中,要控制并发数,前几日我花费了数个小时时间将同事的并发代码梳理了一遍,发现他在所有循环中都用上了并发,去掉不合理的并发后,该接口的效率约莫提了3-4倍,前端调取的等待时间大大减少。
这是一个艰难的历程,并发是双刃剑...
并发另一个值得提的就是和channel的结合使用,同样重要,下次有空再讲~