最近在学习Go语言的特性,看到一个并发的例子特别有意思,在此记录下
func makeThumbnails(filenames <-chan string) int64 {
sizes := make(chan int64)
var wg sync.WaitGroup // number of working goroutines
for f := range filenames {
wg.Add(1) //Add, which increments the counter, must be called before the worker goroutine starts, not within it;
// worker
go func(f string) {
// go func里的defer是要等到最后一行代码(哪怕最后一行代码是异常)执行过后才能执行而最后一行代码恰恰是一个阻塞代码,因为通道不是缓冲通道
defer wg.Done()
thumb, err := thumbnail.ImageFile(f)
if err != nil {
log.Println(err)
return
}
info, _ := os.Stat(thumb) // OK to ignore error
sizes <- info.Size() //这个操作会阻塞,因为是无缓冲通道(应该说这里的操作和下面的range是交错进行的,中间的close是在最后插入执行)
}(f)
}
// 这两个操作只能放在单独的goroutine里 不能放在主goroutine里,如果通道改成容量为3的缓冲通道,则可以放到主goroutine里
// closer
go func() {
wg.Wait()
close(sizes)
}()
// 在对sizes进行for循环前要先关闭,也就是在源代码中去掉close(sizes)是不行,程序原地阻塞住了
var total int64
for size := range sizes {
total += size
}
return total
}
注意,在closer goroutine中,在关闭sizes通道之前,等待所有的工作者结束。这里两个操作(等待和关闭)必须和在sizes通道上面的迭代并行执行。放在循环之前的主goroutine中,因为通道会满,它将永不结束,放在循环后面,没有东西关闭通道,它将不可达。
上面这段是书中原文的解释,但了看几遍后还是不解,于是翻看了英文原版中的解释,仔细对比后发现其中的猫腻,也渐渐理解了原文的解释是什么意思:如果把wait and close
放在main goroutine,那么就会导致每一个goroutine阻塞在
sizes <- info.Size()
,因为sizes是阻塞的并且此时没有goroutine从sizes读取数据。但如果此时有goroutine从sizes中读取数据,就会让前面那个for循环中循环创建的goroutine得以执行,继而defer wg.Done()
得以的执行,那么在单独goroutine中close(sizes)就会执行,close(sizes)执行完就会让最后的for循环执行完毕,不得不说,这个例子实在太巧妙了,理解的关键点在于closer goroutine对于main goroutine来说是立即执行完的,导致在sizes 上的for循环得以执行,才不会让往sizes发送数据的goroutine发生泄露。