這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。在這篇文章中,我們會使用一些 Go 的著名並行範例(Goroutine 和 WaitGroup),高效地遍曆有大量檔案的目錄。所有代碼都可以在 GitHub [這裡](https://github.com/Tim15/golang-parallel-io)找到。我正在開發一個項目,編寫程式來將一個目錄打包成一個檔案。然後,我開始看 Go 的檔案 IO 系統。其中貌似有幾種遍曆目錄的方法。你可以使用 `filepath.Walk()`,或者你可以自己寫一個。[有些人指出](https://github.com/golang/go/issues/16399),與 `find` 相比,`filepath.Walk()` 真的很慢,所以我想知道,我能否寫出更快的方法。我會告訴你我是怎麼使用 Go 的一些很棒的功能來實現的。你可以將它們應用到其他問題上。## 遞迴版本唐納德·克努特(Donald Knuth)曾經寫道:“不成熟的最佳化是萬惡的根源(premature optimization is the root of all evil.)”。遵循此建議,我們首先會用 Go 編寫 `find` 的一個簡單的遞迴版本,然後並行化它。首先,開啟目錄:```gofunc lsFiles(dir string) {file, err := os.Open(dir)if err != nil {fmt.Println("error opening directory")}defer file.Close()```然後,擷取這個檔案中的子檔案切片(Slice,也就是其他語言中的列表或數組)。```gofiles, err := file.Readdir(-1)if err != nil {fmt.Println("error reading directory")}```接著,我們將遍曆這些檔案,並再次調用我們的函數。```gofor _, f := range files {if f.IsDir() {lsFiles(dir + "/" + f.Name())}fmt.Println(dir + "/" + f.Name())}}```可以看到,只有當檔案是一個目錄時,我們才會調用我們的函數,否則,只是列印出該檔案的路徑和名稱。## 初步測試現在,讓我們來測試一下。在一個帶 SSD 的 MacBook Pro 上,使用 `time`,我獲得以下結果:```$ find /Users/alexkreidler274165real0m2.046suser0m0.416ssys0m1.640s$ ./recursive /Users/alexkreidler274165real0m13.127suser0m1.751ssys0m10.294s```並且將其與 `filepath.Walk()` 相比:```gofunc 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/alexkreidler274165real0m13.287suser0m2.033ssys0m10.863s```## Goroutine好了,是時候並行化了。如果我們試著將遞迴調用改為 goroutine,會怎樣呢?只是```goif f.IsDir() {lsFiles(dir + "/" + f.Name())}```改成```goif f.IsDir() {go lsFiles(dir + "/" + f.Name())}```哎呀,不好了!現在,它只是列出一些頂級檔案。這個程式產生了很多 goroutine,但是隨著 main 函數的結束,程式並不會等待 goroutine 完成。我們需要讓程式等待所有的 goroutine 結束。## WaitGroup為此,我們將使用一個 `sync.WaitGroup`。基本上,它會跟蹤組中的 goroutine 數目,保持阻塞狀態直到沒有更多的 goroutine。首先,建立我們的 `WaitGroup`:```govar wg sync.WaitGroup```然後,我們會通過給這個 WaitGroup 加一,利用 goroutine 來啟動遞迴函式.當 `lsFiles()` 結束,我們的 `main` 函數將會在 `wg` 為空白之前都保持阻塞狀態。```gowg.Add(1)lsFiles(dir)wg.Wait()```現在,為我們產生的每一個 goroutine 往 WaitGroup 加一:```goif f.IsDir() {wg.Add(1)go lsFiles(dir + "/" + f.Name())}```然後,在我們的 `lsFiles` 函數尾部,調用 `wg.Done()` 來從 WaitGroup 減去一個計數。```godefer wg.Done()```好啦!現在,在它列印每一個檔案之前,它應該會處於等待狀態了。## ulimits 和訊號量 Channel現在是棘手的部分。根據你的 CPU 以及 CPU 的核心數,你可能會也可能不會遇到這個問題。如果 Go 調度器有足夠的核心可用,那麼它可以充分載入 goroutine([參考這裡](https://stackoverflow.com/questions/8509152/max-number-of-goroutines))。但是,多數的作業系統都會限制每個進程開啟檔案的數目。對於 unix 系統,這個限制是核心 `ulimits`。而在我的 Mac 上,該限制是 10,240 個檔案,但是因為我只有 2 個核心,所以我不會受此影響。在一台最近生產的有更多核心的電腦上,Go 調度器可能會同時建立超過 10,240 個 goroutine。每個 goroutine 都會開啟檔案,因此你會獲得這樣的錯誤:`too many open files`要解決這個問題,我們將使用一個訊號量 channel:```govar semaphoreChan = make(chan struct{}, runtime.GOMAXPROCS(runtime.NumCPU()))```這個 channel 的大小限制為我們機器上的 CPU 或者核心數。```gofunc lsFiles(dir string) {// 滿的時候阻塞semaphoreChan <- struct{}{}defer func() {// 讀取以釋放槽<-semaphoreChanwg.Done()}()...```當我們試圖發送到這個 channel 時,將會被阻塞。然後當完成之後,從該 channel 讀取以釋放槽。詳細資料,請參閱[這個 StackOverflow 文章](https://stackoverflow.com/questions/38824899/golang-too-many-open-files-in-go-function-goroutine)。## 測試和基準```go$ ./benchmark.shCPUs/Cores: 2GOMAXPROCS: 2find /Users/alexkreidler274165real0m2.046suser0m0.416ssys0m1.640s./recursive /Users/alexkreidler274165real0m13.127suser0m1.751ssys0m10.294s./parallel /Users/alexkreidler274165real0m9.120suser0m4.781ssys0m10.676s./walk /Users/alexkreidler274165real0m13.287suser0m2.033ssys0m10.863s```## 總而言之好啦,`find` 仍然是 IO 之王,但至少,我們的並行版本是對原始的遞迴版本和 `filepath.Walk()` 版本的改進。希望這篇文章說明了如何利用 Go 中的一些強大的功能來構建並行系統。我們討論了:* Goroutine* WaitGroup* Channel (訊號量)實際上,在 [github.com/golang/tools/imports/fastwalk.go](https://github.com/golang/tools/blob/master/imports/fastwalk.go) 上,Golang 有一個 `filepath.Walk` 的更快的實現,它的實現原理與本文相同。由於 `filepath` 包中的 API 保證,要在 Go 2.0 版本中才能修改它。
via: https://timhigins.ml/benchmarking-golang-file-io/
作者:Timothy Higinbottom 譯者:ictar 校對:rxcai
本文由 GCTT 原創編譯,Go語言中文網 榮譽推出
本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽
1403 次點擊