golang でループを手っ取り早く並列化する方法

golang (go言語) を用いて for ループを手っ取り早く並列化するにはどうするか、 基本的なテクニック(WaitGroup)を紹介します。

golang (go言語) で書いた以下のようなループ処理があるとします。

func execLoop(list []Item) {
    for _, item := range list {
        do_something(item)
    }
}

list に格納された各 item に対して do_something() を適用する、よくあるタイプのループ処理です。

goroutine で並列化、その副作用

golang ではこの do_something() の適用を超お手軽に並列化できます。あ、もちろん do_something() はリエントラントである前提ですね。

func execLoop(list []Item) {
    for _, item := range list {
        go do_something(item)
    }
}

do_something() の呼び出しを goroutine 化しただけです。ただこれだけだと問題がある場面がほとんどでしょう。

func execLoop(list []Item) {
    for _, item := range list {
        go do_something(item)
    }
}

func main() {
    list := make([]Item, 10)
    execLoop(list)
}

ループが終わってしまって main をも抜けてしまう時 goroutine は実行されません。ちゃんと調べてはいませんけれども、実行されるのを見たことはまだありません。

WaitGroup を導入

つまり goroutine で実行されるすべての do_something() の終了を待つ必要があります。そんなときは sync パッケージの WaitGroup を使いましょう。

import "sync"

func execLoop(list []Item) {
    var wg sync.WaitGroup
    for _, item := range list {
        wg.Add(1)
        go func(item2 Item) {
            do_something(item2)
            wg.Done()
        }(item)
    }
    wg.Wait()
}

一気に複雑になりましたが、まずは wg (=WaitGroup) のメソッドだけに着目してください。

メソッド 適当な説明
Add() WaitGroup のカウンタを上げる
Done() WaitGroup のカウンタを下げる
Wait() WaitGroup のカウンタが0になるまで待つ

こうなってます。つまり各ループの先頭で Add() してループが終わったあとで Wait() すれば、あとは各ループの処理の最後= do_something() のあとで Done() するだけで、待ち合わせ処理の完成です。

書くまでも無いことですが、do_something() は無名関数でラップして item はその引数 item2 として渡してます。こうしないと一度目の実行で item が無名関数に bind されてしまい、意図した動作になりません。

最終版

仕上げに Done() の呼び出しには defer を使って無名関数の先頭でやってしまいましょう。それだけで do_something() で何かあっても無名関数終了時に Done() が呼び出されることが保証されます。

import "sync"

func execLoop(list []Item) {
    var wg sync.WaitGroup
    for _, item := range list {
        wg.Add(1)
        go func(item2 Item) {
            defer wg.Done()
            do_something(item2)
        }(item)
    }
    wg.Wait()
}

execLoop() の修正だけで並列化ができていることがこの記事におけるポイントです。

つまり execLoop() から呼び出す do_something() も、execLoop() を呼び出す側(例えば main)も弄らずに並列化できています。これは「ちょっとループを並列化してパフォーマンスが改善するか見てみよう」という時に使える、スッキリとしたテクニックと言えるでしょう。

いかにも golang らしい性質の一端が垣間見えますね。