示例: 并发的目录遍历

在本小节中,我们会创建一个程序来生成指定目录的硬盘使用情况报告,这个程序和 Unix 里的 du 工具比较相似。大多数工作用下面这个 walkDir 函数来完成,这个函数使用 dirents 函数来枚举一个目录下的所有入口。

ch8/du1
Unresolved include directive in modules/ROOT/pages/ch8/ch8-08.adoc - include::example$/ch8/du1/main.go[]

ioutil.ReadDir 函数会返回一个 os.FileInfo 类型的 slice,os.FileInfo 类型也是 os.Stat 这个函数的返回值。对每一个子目录而言,walkDir 会递归地调用其自身,同时也在递归里获取每一个文件的信息。walkDir 函数会向 fileSizes 这个 channel 发送一条消息。这条消息包含了文件的字节大小。

下面的主函数,用了两个 goroutine。后台的 goroutine 调用 walkDir 来遍历命令行给出的每一个路径并最终关闭 fileSizes 这个 channel。主 goroutine 会对其从 channel 中接收到的文件大小进行累加,并输出其和。

ch8/du1
Unresolved include directive in modules/ROOT/pages/ch8/ch8-08.adoc - include::example$/ch8/du1/main.go[]

这个程序会在打印其结果之前卡住很长时间。

$ go build gopl.io/ch8/du1
$ ./du1 $HOME /usr /bin /etc
213201 files  62.7 GB

如果在运行的时候能够让我们知道处理进度的话想必更好。但是,如果简单地把 printDiskUsage 函数调用移动到循环里会导致其打印出成百上千的输出。

下面这个 du 的变种会间歇打印内容,不过只有在调用时提供了 -v 的 flag 才会显示程序进度信息。在 roots 目录上循环的后台 goroutine 在这里保持不变。主 goroutine 现在使用了计时器来每500ms生成事件,然后用 select 语句来等待文件大小的消息来更新总大小数据,或者一个计时器的事件来打印当前的总大小数据。如果 -v 的 flag 在运行时没有传入的话,tick 这个 channel 会保持为 nil,这样在 select 里的 case 也就相当于被禁用了。

Unresolved include directive in modules/ROOT/pages/ch8/ch8-08.adoc - include::example$/ch8/du2/main.go[]

由于我们的程序不再使用 range 循环,第一个 select 的 case 必须显式地判断 fileSizes 的 channel 是不是已经被关闭了,这里可以用到 channel 接收的二值形式。如果 channel 已经被关闭了的话,程序会直接退出循环。这里的 break 语句用到了标签 break,这样可以同时终结 select 和 for 两个循环;如果没有用标签就 break 的话只会退出内层的 select 循环,而外层的 for 循环会使之进入下一轮 select 循环。

现在程序会悠闲地为我们打印更新流:

$ go build gopl.io/ch8/du2
$ ./du2 -v $HOME /usr /bin /etc
28608 files  8.3 GB
54147 files  10.3 GB
93591 files  15.1 GB
127169 files  52.9 GB
175931 files  62.2 GB
213201 files  62.7 GB

然而这个程序还是会花上很长时间才会结束。完全可以并发调用 walkDir,从而发挥磁盘系统的并行性能。下面这个第三个版本的 du,会对每一个 walkDir 的调用创建一个新的 goroutine。它使用 sync.WaitGroup(§8.5)来对仍旧活跃的 walkDir 调用进行计数,另一个 goroutine 会在计数器减为零的时候将 fileSizes 这个 channel 关闭。

Unresolved include directive in modules/ROOT/pages/ch8/ch8-08.adoc - include::example$/ch8/du3/main.go[]

由于这个程序在高峰期会创建成百上千的 goroutine,我们需要修改 dirents 函数,用计数信号量来阻止他同时打开太多的文件,就像我们在8.7节中的并发爬虫一样:

// sema is a counting semaphore for limiting concurrency in dirents.
var sema = make(chan struct{}, 20)

// dirents returns the entries of directory dir.
func dirents(dir string) []os.FileInfo {
    sema <- struct{}{}        // acquire token
    defer func() { <-sema }() // release token
    // ...

这个版本比之前那个快了好几倍,尽管其具体效率还是和你的运行环境,机器配置相关。


练习 8.9: 编写一个 du 工具,每隔一段时间将 root 目录下的目录大小计算并显示出来。