并发编程的难点在于异常处理。今天我们继续研究缩略图的并发编程,还记得之前留下的问题吗?我们的程序没有对程序返回的错误做特殊照顾。在服务器开发领域,这样的程序的显然不够健壮。
之前我们的 makeThumbnails 函数没有返回值,现在我们给它添加一个 error 类型的返回值。下面这个程序在 demo04.go 中。
func makeThumbnails(filenames []string) error { // errors 是一个无缓冲的 channel errors := make(chan error) 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 } } return nil } func main() { now := time.Now() err := makeThumbnails(os.Args[1:]) if err != nil { fmt.Println(err) } fmt.Printf("elapse:%.3f ms\n", 1000*time.Since(now).Seconds()) }运行一下:
图1 运行结果这次我换成了 4 核心 3.1GHz 的 cpu 主机,所以速度看起来比上一次实验要快很多(上次是 2 核心 2.3 GHz)。
这一次,看起来我们的程序已经支持了错误处理,but … 这个程序还是有 bug.
简单分析一下:
假设 makeThumbnails 函数在处理第一幅图像时出错了,于是在执行
for range filenames { if err := <-errors; err != nil { return err } }的时候,makeThumbnails 会直接返回错误。看起来似乎没什么问题?是的,程序出错了,就应该返回错误。不过我们要讨论的问题不是这里,而是 makeThumbnails 创建的那几个 goroutines:
for _, f := range filenames { go func(f string) { _, err := thumbnail.ImageFile(f) errors <- err }(f) } 假设某个协程在执行 thumbnail.ImageFile 时返回错误,将 err 推入 erros channel,然后该协程结束。for range 部分从 errors 中读取到 err 错误,直接返回。makeThumbnails 已经因为错误返回,其它所有创建的协程将阻塞在 errors <- err 上无法返回,因为 errors 没有缓冲,而且也没有任何协程读取这有什么影响呢?在 Golang 里,这种情况叫 groutine 泄露(goroutine leak)。在服务器开发领域,这是一件非常重要而且严肃的事情,尽管初期它对你的程序功能没什么影响,但是随着运行时间越来越长,最终你的服务就会因为资源耗尽而 crash.
在我们这个例子中,解决这个问题的办法非常简单,我们使用一个带缓冲的 errors 就行了,你可以修改成下面这样:
func makeThumbnails(filenames []string) error { // errors 是一个带缓冲的 channel,大小和要处理的文件个数一致 errors := make(chan error, len(filenames)) 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 } } return nil }这样即便 makeThumbnails 因为错误提前返回,其它协程也能正常结束。
