詩と創作・思索のひろば

ドキドキギュンギュンダイアリーです!!!

Fork me on GitHub

Go 言語における並行処理の構築部材

5年前に買った『Java並行処理プログラミング ―その「基盤」と「最新API」を究める―』をようやく読んだ。買った頃には Perl やシンプルな JavaScript ばかり書いていたので並行プログラミングなんてほとんど気にすることがなく、実感がなくて読むのも途中で止まってしまっていた本で、家を掃除しているときに見つけたもの。その後も趣味で Android アプリを書くなど Java に触れる機会はあったけれど、せいぜいが AsyncTask を使うくらいで、マルチスレッドを強く意識してコードを書くこともなかった。

Java並行処理プログラミング ―その「基盤」と「最新API」を究める―

Java並行処理プログラミング ―その「基盤」と「最新API」を究める―

ところがここ数年で Go を書きはじめて以来、それと気づかぬ間に並行処理をプログラムするようになっていたのがよかったらしく、今だとあれこれのトピックを身近に感じて興味深く読めた。Go の race detector が優秀で、テキトーなコードの瑕疵を指摘されているうちに感覚が身についてきたのだと思う。

Share Memory By Communicating」というスローガンでよく表わされるように、Go の並行 API はチャンネルと goroutine によるもので、かなり簡素にできている。一方 Java はsynchronized 修飾子をはじめ、Object#wait / notify とか、インタラプトの仕組みなど、フレームワークとも呼べるような仕組みが用意されていて、対照的だった。しかしそうは言っても goroutine 間で変数を共有したいことは Go を書いていても頻繁にあるわけで、参考になるところは多い。

この本では正しい並行処理を行うために、

  • データの不変項をカプセル化し、それを構成する変数を同じロックでガードすること
  • API の同期化ポリシーをドキュメントすること

を推奨している。特に後者。利用者は API の形式的な情報からそのスレッドセーフ性を判断することができないからだ。Go の標準ライブラリでも、並行処理に関わりそうなところではその旨ドキュメントされているみたいだ。

% go doc strings.Replacer
type Replacer struct {
        // Has unexported fields.
}
    Replacer replaces a list of strings with replacements. It is safe for
    concurrent use by multiple goroutines.
% go doc net/http2.Framer.WriteData
func (f *Framer) WriteData(streamID uint32, endStream bool, data []byte) error
    WriteData writes a DATA frame.

    It will perform exactly one Write to the underlying Writer. It is the
    caller's responsibility to not call other Write methods concurrently.

以下、一般的な並行処理の構築部材(ビルディングブロック)を Go でどう実現するかのパターンをメモしておく。Go 的にはこういった処理はライブラリ化するのではなくコピペもしくはイディオムとして憶えておくのが正しい態度だろうので、過度の一般化をしようとしてはいけない。

見聞きして知っていることをもとにできるだけ真摯に書くが、現実世界の並行プログラミングに詳しいわけではないので間違っていることもあるかもしれない。

ミューテックス(Mutex)

ミューテックスは複数のスレッド間で共有するリソースのアクセス権を管理する部品。これはそのまま sync.Mutex というのが存在する。チャンネルに依らない同期を行うのにたいへん重要なでプリミティブな部品だ。

sync.Mutex は zero value が未ロックを表していて、特別な初期化を必要としないので構造体に埋め込むことができる。

type syncMap struct {
    sync.Mutex
    m map[string]string
}

など。この構造体 s のフィールドを変更する前と後に s.Lock()s.Unlock() を呼ぶよう徹底できていれば、この構造体は goroutine セーフだと言える。もちろんこれらのフィールドはプライベートにしておくべきである。

構造体を作るほどではない時は、守りたい変数と並べて宣言しておく。同じ括弧の中に入れるとよりよいと思う。Mutex の Go における短い名前は「mu」である

var (
    cache map[string]string
    cacheMu sync.Mutex
)

Read-Write ロックを実現するための RWMutex というのもある。

ラッチ(Latch)

ラッチは条件が整うまでスレッドの実行を留めておくための部品。たとえば全ての HTTP GET が終わってデータが揃うまでは待機する、というようなときに使う。

これは Go においては sync.WaitGroup という名前で提供されている。これも zero value で意味を持つので、var wg sync.WaitGroup と宣言して使う。

典型的には以下のように使われる。

var wg sync.WaitGroup
for _, arg := range args {
    wg.Add(1)
    go func(arg string) {
        longRunningTask(arg)
        wg.Done()
    }(arg)
}
wg.Wait()
...

WaitGroup の Go における短い名前は「wg」である。ちなみに複数の goroutine を起動してすべての処理が完了するまで待つとき、その数があらかじめわかっている場合は、結果を通知するチャンネルを利用することもある。

c := make(chan struct{}, 4)
for i := 0; i < 4; ++i {
    go doJob(i, c) // 終わったら c <- struct{}{} される
}
for i := 0; i < 4; ++i {
    <-c
}

ブロッキングキュー(Blocking Queue)

有限または無限のキャパシティを持つキューで、キャパシティを越えて追加しようとするとき及び空の状態で取り出そうとするときにブロックする。2つのプロセスでリソースの所有権を移動しながら同期化できる。

Go おいてはこれこそがチャンネル(chan)。自明なのでコードは特になし。チャンネルの Go における短い名前は「c」である(「ch」もないわけではない)。

セマフォ(Semaphore)

(計数)セマフォは決まった数のプロセスだけが同時に並行に走ってよい、という制限を設けるためのもの。たとえば過剰な IO を避けるために利用される。

Go においてセマフォはキャパシティありのチャンネルとして実装できる。

var sem = make(chan struct{}, maxConcurrency)

func process(arg string) {
    sem <- struct{}{}
    ...
    <-sem
}

func run() {
    for _, arg := range args {
        go process(arg)
    }
}

これ初めて見たときはよく分かんなかった。セマフォの Go における短い名前は「sem」である

Future

Future (カタカナで書かない気がする)は後々必要になる値の計算や IO をあらかじめスタートさせておきたいような場合に使うパターン。結果を取得するときにブロックさせて待つこともできる。自分は実際に書いたり読んだりした経験はないのだけど、Future パターンを意識して書くならこんな風に書けると思う。

struct futureInt {
    result int
    done chan struct{}
}

func (fut *futureInt) Get() int {
    <-fut.done
    return fut.result
}

func calcInt() futureInt {
    fut := futureInt{
        done: make(chan struct{}),
    }
    go func() {
        fut.result = longRunningTask()
        close(fut.done)
    }
    return fut
}

バリヤ(Barrier)

こちらは参考までに記しておく。Java の CyclicBarrier というクラスは再利用のできるラッチのようなもので、スレッド群を繰り返し同期化するような場合に便利らしい。

検索すると sync.Cond を使ったあまり簡単ではない例も見つかるのだが、golang-dev には goroutine は十分軽量だから毎回新しいのを起動したらいいよ といった話もあり、そっちのほうが Go らしいようにも思った。

まとめ

プログラム言語の話に限らず、ある領域の見聞や経験を深めたあとでは他の領域から得られる知識の吸収の仕方の質が異なるもので、思ったより参考になる話が多かった。とはいえ Go には Go のやり方があるものなので、あまり Java におけるクラスやパターンをそのまま流用しようとするのはよくない傾向であることも意識しておかなければならない。Go を読み書きする際には(パターンというほどでもない)イディオムを知っておくことに意味がある言語なので、こうやってまとめておくことも無駄ではないだろうと思う。

参考文献

Java並行処理プログラミング ―その「基盤」と「最新API」を究める―

Java並行処理プログラミング ―その「基盤」と「最新API」を究める―

増補改訂版 Java言語で学ぶデザインパターン入門 マルチスレッド編

増補改訂版 Java言語で学ぶデザインパターン入門 マルチスレッド編

はてなで一緒に働きませんか?