んだ日記

ndaDayoの技術日記です

Go言語による並行処理3章 WaitGroupのことなどについて

さて、今回からはいよいよGo言語による並行処理を読んでいきます。

Go言語による並行処理を読みながら公式Docを読んだりしながらのメモです。今回は、序盤とWaitGroupのことなどについて整理しました。

ゴルーチン

ゴルーチンについて整理する前に、コルーチンとサブルーチンという概念について整理します。

コルーチンとサブルーチン

筑波大学 マルチスレッド・プログラミング(2)/ Multithread programming (2)

まずは、Wikipediaを見てみましょう。

サブルーチンがエントリーからリターンまでを一つの処理単位とするのに対し、コルーチンはいったん処理を中断した後、続きから処理を再開できる。

コルーチンは、プリエンティブではないとあります

コルーチンは、プリエンティブではないということは、CPUを自発的に手放して中断と再開を行うことができるということですな。

ここらへん、あーなるほどとなるのは、Linuxカーネルを読んだ成果が出てるくさいですね

しかし、サブルーチンとてカーネル自体がプリエンティブな方式なら中断と再開は当然しているんじゃない?と思ったのですが、疑問。

一旦、コルーチンとサブルーチンを押さえた上で、ゴルーチンについて理解します

ゴルーチンは特殊なコルーチン

本書では、「ゴルーチンは特殊なコルーチン」と説明しています。

では、何が特殊なのでしょうか?それは、Goのランタイムと密結合している点において特殊、ということです。

Goのランタイムと密結合している

Goのランタイムと密結合しているとは、

Goのランタイムはゴルーチンの実行時の振る舞いを観察し、ゴルーチンがブロックしたら自動的に一時停止し、ブロックが解放されたら再開します

Goのランタイムが良しなにゴルーチンの中断と再開をやってくれるというわけですね。

本書では、コルーチンは単に「プリエンティブではない」並行処理のサブルーチンです、と述べてますね。

ゴルーチンは、「プリエンティブではない」並行処理のサブルーチンというだけでなく、ランタイムと密結合である点が特殊だということですね。

グリーンスレッド

グリーンスレッドというワードについても、理解しておきます。

ユーザランドのソフトウェアが独自に用意したスレッド機構は、一般的にグリーンスレッドと呼ばれている。グリーンスレッドは、OSのスレッドに比べて、スレッド生成と破棄のコストを小さくできるため、Erlang、Go、Haskellといった、並行プログラミングを得意とする処理系で用いられている。

並行プログラミング入門 6.2より

ユーザ空間、ここではランタイムが独自に用意したスレッド機構ですね。OSにもスレッドがありますが、それとは全くの別物。

で、ゴルーチンは、必ずしもグリーンスレッドではないと本書では述べてます。

fork-joinモデル

プロセスの生成のところで、fork()とexec()システムコールを学んだが、それに近い。近い、というだけで、同じようにメモリ管理を行っているわけではなく、論理的に近いという感じです。

fork-joinモデルは、端的にいえば、分岐と合流。親から分岐(fork)した子が、再び合流(join)するというモデルで動いています。

Package sync

ここからは、公式Docも参考にしながら、Package sync について理解していきます。

Overview

sync package - sync - Go Packages

Package sync provides basic synchronization primitives such as mutual exclusion locks. Other than the Once and WaitGroup types, most are intended for use by low-level library routines

syncパッケージは、相互排他ロックなどの基本的な同期のための機能を提供するパッケージらしい。

そのまんま。

WaitGroup

sync package - sync - Go Packages

A WaitGroup waits for a collection of goroutines to finish.

WaitGroupは、goroutinesの集合が完了するのを待つ。と定義されておりますね。

雰囲気よりコードということで、WaitGroup structのメソッドを見てみましょう。

type WaitGroup struct {
    noCopy noCopy

    state atomic.Uint64 // high 32 bits are counter, low 32 bits are waiter count.
    sema  uint32
}

stateがどうやらカウンターらしい。

そんで、以下の3つのメソッドがあります。

func (wg *sync.WaitGroup).Add(delta int)  
func (wg *sync.WaitGroup).Done()   
func (wg *sync.WaitGroup).Wait()  

Add

Addは、待機するgoroutineを設定します。 内部的には、WaitGroupのカウンターに引数deltaを追加している。

// src/sync/waitgroup.go LINE:52
state := wg.state.Add(uint64(delta) << 32)

で、もしもカウンターがゼロの場合には、Waitでブロックされていたすべてのゴルーチンを解放する

// src/sync/waitgroup.go LINE:78
// Reset waiters count to 0.
    wg.state.Store(0)
    for ; w != 0; w-- {
        runtime_Semrelease(&wg.sema, false, 0)
    }

お作法とルールとしては、

  • Addの呼び出しは監視対象のゴルーチンの外で行われている
  • Addの呼び出しはできる限り監視対象のゴルーチンの直前に書く

です。

Wait

順番前後しますが、続いてWait。Waitは、すべてのゴルーチンが完了するまでブロックする。

Done

最後にDone

Doneはシンプル。Addを呼び出して、カウンターを1つ減らしているだけ。

// Done decrements the WaitGroup counter by one.
func (wg *WaitGroup) Done() {
    wg.Add(-1)
}

fork-joinモデル 再び

では、syncパッケージのWaitGroupを押さえた上で以下のサンプルコードを見てみましょう。

var wg sync.WaitGroup
sayHello := func() {
    defer wg.Done()
    fmt.Println("hello")
}
wg.Add(1)
go sayHello()
wg.Wait() 

まずは、wg.Add(1)でWaitGroupのカウンターを一つ増やします。Wait()でグループのすべてのゴルーチンの完了するまでブロックします。

defer wg.Done()で関数を抜ける時に、カウンターを一つデクリメントすることで、カウンタが0になりすべてのゴルーチンが完了するという流れですね。

まとめ

今回は、ゴルーチンの定義やらWaitGroupのことなどについてまとめてみました。

Linuxのプロセス管理について、ざっくりと理解しておいたおかげで、だいぶスムーズに読めるようになっていました。

学びが深い一冊になりそうです。

次回は、MutexとRWMutexのあたりから進めてまいりたいと思います。

僕から以上。あったかくしてねろよー。