[go语言]goroutine的一种使用场景

近日在使用go语言写一个文件转换的工具,使用到了goroutine来提高处理效率。在写代码的过程中,对goroutine的使用方式经历了三个版本的变动,我觉得有必要记录和总结一下,也需要不断思考怎么样才是用go语言的思考去写go语言代码。Write your Go code in a Go way.

目的:遍历一个目录及其子目录,把某些特定文件转换到另外一种文件保存到指定目录下。因为文件可能有很多,不想每个文件都对应一个goroutine一下子有非常多goroutine。毕竟CPU的核是固定数量的。虽然同时存在很多很多goroutine也不会同时被执行导致物理线程切换代价高,但是毕竟占了好多内存,而且数量不太能预期。所以打算寻找一种方法能尽量提高CPU使用效率,同时又控制goroutine的数量。

第一个版本:
并发方式是采用经典的生产者消费者模型,一个生产者把遍历到的文件路径通过消息队列交给多个在此消息队列上等待的消费者。

// 遍历指定目录,把符合条件的文件路径转给回调函数collect,实现略。
func walk(path string, collect func(string))
// 文件转换,实现略。
func convert(file string)

进行处理。
func main() {
var wg sync.WaitGroup
// 消息队列
ch := make(chan string, runtime.NumCPU())
// 创建消费者
for i := 0; i < runtime.NumCPU(); i++ {
go func() {
for {
file := <- ch
convert(file)
wg.Done()
}
}()
}
// 生产者
walk(path, func(file string){
wg.Add(1)
ch <- file
})
// 等待所有文件处理完成
wg.Wait()
}
第一个版本的问题是处理逻辑分离了,很明显的分裂成两部分生产者逻辑和消费者逻辑,而我的目的是想让代码逻辑尽量的看上去串行化,像一个顺序程序一样,这样就比较清晰易懂。所以需要尽量把生产者逻辑和消费者的逻辑放在一起。

第二个版本:
还是消息队列,但是消息队列中的消息不是数据,而是函数。消费者只要执行这个函数,不需要关心这个函数的实现和它的业务逻辑。
func main() {
var wg sync.WaitGroup
// 消息队列
ch := make(chan func(), runtime.NumCPU())
// 创建消费者
for i := 0; i < runtime.NumCPU(); i++ {
go func() {
for {
f := <- ch
f()
}
}()
}
// 生产者
walk(path, func(file string){
wg.Add(1)
ch <- func() {
convert(file)
wg.Done()
}
})
// 等待所有文件处理完成
wg.Wait()
}
这个版本好多了,获取文件和处理文件的业务逻辑都在一起,不会导致思维上的跳跃。但是能不能更好一些呢?那个消费者goroutine的代码既然和业务逻辑没有直接关系,有没有可能精简掉?

第三个版本:
第三个版本把消息队列去掉了,换成数量有限的令牌,要处理一个文件必须先获得令牌,使用完成后再归还。获得令牌是阻塞的,如果暂时没有令牌可以获得就会阻塞在上面,避免产生过多的数量不可控的goroutine。令牌和消息队列一样还是用channel实现,但是意义已经不一样了。
func main() {
var wg sync.WaitGroup
// 生成指定数量的令牌
tokens := make(chan int, runtime.NumCPU())
walk(path, func(file string){
tokens <- 1 // 获取令牌
wg.Add(1)
go func() {
convert(file)
wg.Done()
<-tokens // 归还令牌
}()
})
// 等待所有文件处理完成
wg.Wait()
}
现在的代码就比较清晰直观,而且代码行也少了许多。

本文来自:新浪博客

感谢作者:stevewang

查看原文:[go语言]goroutine的一种使用场景

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。