『GCTT 出品』并行化 Golang 文件 IO

Posted Go语言中文网

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了『GCTT 出品』并行化 Golang 文件 IO相关的知识,希望对你有一定的参考价值。

在这篇文章中,我们会使用一些 Go 的著名并行范例(Goroutine 和 WaitGroup),高效地遍历有大量文件的目录。所有代码都可以在 GitHub 这里找到。

我正在开发一个项目,编写程序来将一个目录打包成一个文件。然后,我开始看 Go 的文件 IO 系统。其中貌似有几种遍历目录的方法。你可以使用 filepath.Walk(),或者你可以自己写一个。有些人指出,与 find 相比,filepath.Walk() 真的很慢,所以我想知道,我能否写出更快的方法。我会告诉你我是怎么使用 Go 的一些很棒的功能来实现的。你可以将它们应用到其他问题上。

递归版本

唐纳德·克努特(Donald Knuth)曾经写道:“不成熟的优化是万恶的根源(premature optimization is the root of all evil.)”。遵循此建议,我们首先会用 Go 编写 find 的一个简单的递归版本,然后并行化它。

首先,打开目录:

func lsFiles(dir string) {
    file, err := os.Open(dir)
   if err != nil {        fmt.Println("error opening directory")    }
   defer file.Close()

然后,获取这个文件中的子文件切片(Slice,也就是其他语言中的列表或数组)。

files, err := file.Readdir(-1)
if err != nil {    fmt.Println("error reading directory") }

接着,我们将遍历这些文件,并再次调用我们的函数。

    for _, f := range files {
       if f.IsDir() {            lsFiles(dir + "/" + f.Name())        }        fmt.Println(dir + "/" + f.Name())    } }

可以看到,只有当文件是一个目录时,我们才会调用我们的函数,否则,只是打印出该文件的路径和名称。

初步测试

现在,让我们来测试一下。在一个带 SSD 的 MacBook Pro 上,使用 time,我获得以下结果:

$ find /Users/alexkreidler
    274165

real    0m2.046s
user    0m0.416s
sys    0m1.640s

$ ./recursive /Users/alexkreidler
    274165

real    0m13.127s
user    0m1.751s
sys    0m10.294s

并且将其与 filepath.Walk() 相比:

func main() {
    err := filepath.Walk(os.Args[1], func(path string, fi os.FileInfo, err error) error {
       if err != nil {
           return err        }        fmt.Println(path)
       return nil    })
   if err != nil {        log.Fatal(err)    } }
./walk /Users/alexkreidler
    274165

real    0m13.287s
user    0m2.033s
sys    0m10.863s

Goroutine

好了,是时候并行化了。如果我们试着将递归调用改为 goroutine,会怎样呢?

只是

if f.IsDir() {
    lsFiles(dir + "/" + f.Name())
}

改成

if f.IsDir() {
   go lsFiles(dir + "/" + f.Name()) }

哎呀,不好了!现在,它只是列出一些顶级文件。这个程序生成了很多 goroutine,但是随着 main 函数的结束,程序并不会等待 goroutine 完成。我们需要让程序等待所有的 goroutine 结束。

WaitGroup

为此,我们将使用一个 sync.WaitGroup。基本上,它会跟踪组中的 goroutine 数目,保持阻塞状态直到没有更多的 goroutine。

首先,创建我们的 WaitGroup

var wg sync.WaitGroup

然后,我们会通过给这个 WaitGroup 加一,利用 goroutine 来启动递归函数.当 lsFiles() 结束,我们的 main 函数将会在 wg 为空之前都保持阻塞状态。

wg.Add(1)
lsFiles(dir)
wg.Wait()

现在,为我们产生的每一个 goroutine 往 WaitGroup 加一:

if f.IsDir() {
    wg.Add(1)
   go lsFiles(dir + "/" + f.Name()) }

然后,在我们的 lsFiles 函数尾部,调用 wg.Done() 来从 WaitGroup 减去一个计数。

defer wg.Done()

好啦!现在,在它打印每一个文件之前,它应该会处于等待状态了。

ulimits 和信号量 Channel

现在是棘手的部分。根据你的 CPU 以及 CPU 的内核数,你可能会也可能不会遇到这个问题。如果 Go 调度器有足够的内核可用,那么它可以充分加载 goroutine(参考这里)。但是,多数的操作系统都会限制每个进程打开文件的数目。对于 unix 系统,这个限制是内核 ulimits。而在我的 Mac 上,该限制是 10,240 个文件,但是因为我只有 2 个内核,所以我不会受此影响。

在一台最近生产的有更多内核的计算机上,Go 调度器可能会同时创建超过 10,240 个 goroutine。每个 goroutine 都会打开文件,因此你会获得这样的错误:

too many open files

要解决这个问题,我们将使用一个信号量 channel:

var semaphoreChan = make(chan struct{}, runtime.GOMAXPROCS(runtime.NumCPU()))

这个 channel 的大小限制为我们机器上的 CPU 或者核心数。

func lsFiles(dir string) {
   // 满的时候阻塞    semaphoreChan <- struct{}{}
   defer func() {
       // 读取以释放槽        <-semaphoreChan        wg.Done()    }()    ...

当我们试图发送到这个 channel 时,将会被阻塞。然后当完成之后,从该 channel 读取以释放槽。详细信息,请参阅这个 StackOverflow 帖子。

测试和基准

$ ./benchmark.sh
CPUs/Cores: 2
GOMAXPROCS: 2
find /Users/alexkreidler
   274165

real   0m2.046s user   0m0.416s sys   0m1.640s ./recursive /Users/alexkreidler
   274165

real   0m13.127s user   0m1.751s sys   0m10.294s ./parallel /Users/alexkreidler
   274165

real   0m9.120s user   0m4.781s sys   0m10.676s ./walk /Users/alexkreidler
   274165

real   0m13.287s user   0m2.033s sys   0m10.863s

总而言之

好啦,find 仍然是 IO 之王,但至少,我们的并行版本是对原始的递归版本和 filepath.Walk() 版本的改进。

希望这篇文章说明了如何利用 Go 中的一些强大的功能来构建并行系统。我们讨论了:

* Goroutine
* WaitGroup
* Channel (信号量)

实际上,在 github.com/golang/tools/imports/fastwalk.go 上,Golang 有一个 filepath.Walk 的更快的实现,它的实现原理与本文相同。由于 filepath 包中的 API 保证,要在 Go 2.0 版本中才能修改它。


via: https://timhigins.ml/benchmarking-golang-file-io/

本文由 GCTT 原创编译,Go 中文网 荣誉推出

以上是关于『GCTT 出品』并行化 Golang 文件 IO的主要内容,如果未能解决你的问题,请参考以下文章

『GCTT 出品』在 golang 中如何调用私有函数(绑定隐藏的标识符)

『GCTT 出品』用不到 100 行的 Golang 代码实现 HTTP(S) 代理

『GCTT 出品』你所不知道的 Go 语言的一些令人惊叹的优点

『GCTT 出品』使用 Go 语言写一个即时编译器(JIT)

GCTT 出品 | 阅读挑战:Go 的堆排序

『GCTT 出品』如何写 Go 中间件