以前、こんな記事を書いた。
Big Sky :: 日本語grepが出来るjvgrepというのを作った。
http://mattn.kaoriya.net/software/lang/go/20110819203649.htm
実は jvgrep を作った当初、処理がかなり遅かった。まぁ複数のエンコーディングを試すからしょうがないよね程度に思ってたけど、どうにか速くならないかと思い、処理の並行化を行ってパフォーマンスを向上させた。この記事はその時にやった改善策。
jvgrep は
-R
オプションや
**/*
で再帰検索する機能が付いているんだけど、これを行う場合
という処理が走る事になる。
しかしながら結果の順番を守ろうと考えた場合、find と grep を安直に同時に走らせる訳にはいかなくなる。走らせると結果が交錯してしまうからだ。
こういうのを行う場合、C言語だとFIFOキューとスレッドを作り、find 側が push、grep 側が pop を行う仕組みを作る。
しかしながらメモリの増加を管理したり、grep 側が空きになった時に待機する処理ってのを考えると、C言語だと結構めんどくさかったりする。
Go言語はこのあたりが非常に簡単に実装出来る様になっている。
まず find 部と grep 部の処理を分割し、grep 部を FIFO キューに対して連続で呼び出せる様にする。
func GoGrep(ch chan *GrepArg, done chan int) {
for {
arg := <-ch
if arg == nil {
break
}
Grep(arg)
}
done <- 1
}
Grep が本体だが今回の話の本質ではないので省略。引数の ch に Grep が使う引数が飛び込んで来る。全ての grep 対象が完了するか途中終了する場合には find 側から nil を渡すというお約束にした。
メインコントローラ側で、この FIFO となるチャネルを作る。
ch := make(chan *GrepArg)
done := make(chan int)
なぜ二つチャネルを作っているかというと、find 側が先に終了してしまった場合にプログラムが終了しない様、待機する為で、上記の GoGrep の最後に 1 を渡している。
チャネルを作ったら GoGrep をバックグラウンドで起動する。
go GoGrep(ch, done)
Go言語は関数呼び出しに
go
を付けるだけで非同期に実行してくれる。
次に find 部。Go言語の filepath パッケージにはファイルが見つかる度にコールバック関数を呼び出してくれる Walk がある。
filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
if info == nil {
return err
}
path = filepath.ToSlash(path)
if ere != nil && ere.MatchString(path) {
if info.IsDir() {
return filepath.SkipDir
}
return nil
}
if fre.MatchString(path) {
ch <- &GrepArg{pattern, path, }
}
})
検索対象のファイルが見つかった場合、先ほど作成した ch に Grep 引数を渡している。
find 側は全てのファイルを検索し終えたら grep 側に処理の終了を伝える。
ch <- nil
そして grep の終了(done)を待つ。
<-done
図解すると
こんなイメージになる。find もフォルダを再帰的に検索すると結構重くなるので各処理を並行実行させる事でかなりのパフォーマンス向上が得られた。
最初から find 部と grep 部が関数分けされていれば、もっと簡単に並行処理の実装が出来た事になる。