接下来了几篇文章,我们会重点讨论有关 Go 的并发编程以及常见的技术手段。当然,所有问题我们都需要从实例出发,所有的实例均来源于《The Go Programming Language》一书的第 8 章。
1. 缩略图计算程序
这个例子的目的很简单,给定多幅 jpg 图片,通过调用 Go 里的缩略图处理函数,生成相应的缩略图存储到本地。我们的目的不是写一个如何将图像处理成缩略图的函数,所以,你不必再大废周折的再写一套这样的函数。
有关缩略图的计算程序,在《The Go Programming Language》附带的源码里已经提供,我们好好的做调包侠就行。如果你觉得下载他的源码很麻烦,你就用我这里已经准备好的代码:
https://gitee.com/ivan_allen/gopl (代码路径 gopl/goroutine/thumbnail
). 另外,我还为你精心准备了 4 幅 jpg 图片放在该目录下,省去你找图片的时间。
一切准备就绪后,我们的第一个示例如下:
上面这段程序有必要先解释一下:
- 第二行注释
// +build ignore
是有必要存在的。这里需要它的原因是我们把 main 包和 thumbnail 写到了同一个文件夹下面。如果没有这一行你使用go run demo01.go
运行就会失败。换句话说,这行注释是写给 go 编译器看的。未来我们会学习到更多类似这样的注释,这里知道即可。 -
makeThumbnails
函数的参数是一个 string 数组,保存的是图片文件名称。这个函数将为所有图片文件生成缩略图 -
elapse
函数是一个结合 defer 语法和闭包的时间打点函数,很久以前你就学习过了,千万别在问我原理。在这里我们使用elapse
函数来统计makeThumbnails
函数的运行时间。
OK,一切准备就绪,我们就来运行一下看看:
在我机器上会输出:
同时你的文件夹下面会生成 1.thumb.jpeg 2.thumb.jpeg 3.thumb.jpeg 4.thumb.jpeg
这几个缩略图文件。
下面是我统计的一个生成不同数量缩略图时的耗时情况:
图1 不同数量图片执行耗时
可以看到,随着数量增加,耗时也在增加。因为在上面的代码里,我们的程序是串行计算的。换句话说,我们的程序是在计算完 1.jpeg 后,再计算 2.jpeg。
很明显,串行计算并不能充分的发挥多核 cpu 的优势。再说了,生成这 4 幅 jpg 图片的缩略图顺序对我们来说根本无关紧要,而且它们之间也不会相互影响。既然如此,我们就可以——开启 goroutine,并发的去生成每幅图的缩略图。
2. 并发计算缩略图
2.1 版本一
接下来,我们把重心放到 makeThumbnails 函数上面。我们把新的程序保存在 demo02.go 中。
不幸的是,这个程序有 2 个 bug. 先来看看怎么回事:
图2 并行计算缩略图
这里使用了不同数量的图片,结果发现无论几张图片,消耗的时间都非常短~~~这并行计算提供幅度也太高了吧?先不要偷着乐,这肯定出问题了。在图 1 里我们看到计算一张图要 18ms 左右,现在计算 4 张图却只要 0.003 ms. 另一方面,缩略图也确实没有生成。
2.2 版本二
我们第一次在并行编程上就遇到了挫折。冷静下来先分析一下:
makeThumbnails 函数里开启了 goroutine 后,迅速返回了。然而开启的 goroutine 还没来得及执行,main 函数就已经执行结束。
这是我们发现的第一个 bug. 解决方案有很多,如果你还记得我 channel 的特性的话,利用 channel 会非常容易解决。下面是改进后的 demo03.go 版本,这里我就只粘贴 makeThumbnails 函数,顺带我们还解决了第 2 个 bug.
分析一下:
- bug 1 我们通过 channel 解决了 goroutine 还没来得及开始就结束的问题。原理很简单,这里利用了 channel 的特性。
- bug 2 是一个称之为循环变量快照的问题(The problem of loop variable capture)。如果不通过传参的形式,结果会导致所有的 goroutine 看到的都是同一个 f,这样最后你就只能生成最后一个缩略图了。
循环变量快照问题在很多支持闭包的语言里都会出现,不仅仅是 Golang.
OK,来看看这个『正确』的版本耗时如何?
图3 『正确』的并行计算程序
可以看到这个版本的计算看下来正常了很多(确实也是正确的),相比图 1 中的版本,这个版本要快了很多。
3. 总结
- 深入理解 goroutine
- 掌握如何使用 channel 进行协程同步
- 理解什么是循环变量快照
如果你足够细心的话,你会发现图 3 的标题,我给正确两字加了引号。不是说我们写的程序是错误的,而是我们对这个程序处理的还不够仔细——我们没有处理程序出错的情况,这显然不够。说的夸张点,在程序开发里,几乎有一半以上的代码都是在处理异常,所以衡量程序好不好,一个重要的指标就看程序对异常处理是否足够优雅和仔细,而不仅仅是看功能是否实现,复杂度等这些因素。