目次
1 章 並行処理入門
1.1 ムーアの法則、Web スケール、そして私たちのいる混沌
1.2 なぜ並行処理が難しいのか
1.2.1 競合状
1.2.2 アトミック性
- 何かがアトミック、あるいはアトミック性があると考えられる場合、それが操作されている特定のコンテキストの中では分割不能、あるいは中断不可であること意味する
- あるコンテキストの中ではアトミックであっても、別のコンテキストでもそうであるとは限らない
- ある操作のアトミック性というのは、現在注目しているスコープによって変わりえる
- 操作がアトミックになるかどうかは、アトミックにしたいコンテキストへ依存する
- コンテキストに 1 つも並行処理がないプログラムであれば、このコードはそのコンテキスト内ではアトミック
- 他のゴルーチンに公開しないようなコンテキストのゴルーチンだったら、このコードはアトミック
1.2.3 メモリアクセス同期
- クリティカルセクションが繰り返されていないか
- クリティカルセクションの大きさはどれほどに留めるべきか
1.2.4 デッドロック、ライブロック、リソース枯渇
- デッドロックの予防は難しい
- ライブロックとは並行操作を行っているけれど、その操作はプログラムの状態をまったく進めていないプログラム。2つのプロセスが協調なしにデッドロッグを予防しようとした結果おこることがある。
- 貪欲な処理と行儀の良い処理があるときに、貪欲な処理がリソースを専有しすぎるあまり、行儀の良い処理が非効率的に見える
1.2.5 並行処理の安全性を見極める
- 誰が並行処理を担っているか。
- 問題空間がどのように並行処理のプリミティブに対応しているか。
- 誰が同期処理を担っているか。
1.3 複雑さを前にした簡潔さ
2 章 並行性をどうモデル化するか:CSP とは何か
2.1 並行性と並列性の違い
- 並行性はコードの性質を指し、並列性は動作しているプログラムの性質を指す
2.2 CSP とは何か
- CSP は「Communicating Sequential Processes」の略
- Hoare はプロセスという用語を、必要な入力を処理し、他のプロセスが消費する出力をもたらすロジックの塊をカプセル化するもの
2.3 これがどう役に立つのか
- ゴルーチンは問題空間を並列性の観点で考えなければならない状況から解放し、かわりにそのような問題を自然な並行性の問題として構築できる
2.4 Go の並行処理における哲学
- ある瞬間にただ 1 つのゴルーチンがある特定のデータの責任を持つように心がけると良い
- sync パッケージでロックするか、チャネルを使うかの判断基準
3 章 Go における並行処理の構成要素
3.1 ゴルーチン(goroutine)
- ゴルーチンは Go のプログラムでの最も基本的な構成単位
- すべての Go のプログラムには最低 1 つのゴルーチンがある → メインゴルーチン
- コルーチンは単に「プリエンプティブでない」並行処理のサブルーチン(Go では関数、クロージャー、メソッドに相応)だが、ゴルーチンは Go のランタイムと密結合している
3.2 sync パッケージ
- WaitGroup はひとまとまりの並行処理があったとき、その結果を気にしない、もしくは他に結果を収集する手段がある場合に、それらの処理の完了を待つ手段
- WaitGroup を並行処理で安全なカウンターと考えることもできる
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println("1st goroutine sleeping...")
time.Sleep(1)
}()
wg.Add(1)
go func() {
defer wg.Done()
fmt.Println("2nd goroutine sleeping...")
time.Sleep(2)”
}()
wg.Wait()
fmt.Println("All goroutines complete.")”
// 2nd goroutine sleeping...
// 1st goroutine sleeping...
// All goroutines complete.”
- ミューテックス(Mutex)は「相互排他」を表す英語の”mutual exclusion” の略で、プログラム内のクリティカルセクションを保護する方法の 1 つ
- “チャネルは通信によってメモリを共有し、Mutex は開発者が守らなければならないメモリに対する同期アクセスの慣習を作ることでメモリを共有する
- sync.RWMutex は概念的には Mutex と同じ
- RWMutex は書き込みのロックをしているものがいなければ、任意の数の読み込みのロックが取れる
- Cond はゴルーチンが待機したりイベントの発生を知らせるためのランデブーポイント
// sleepでCPUを待機させる
for conditionTrue() == false {
time.Sleep(1*time.Millisecond)
}
// Waitで別のごルーチンを待機させる
c := sync.NewCond(&sync.Mutex{})
c.L.Lock()
for conditionTrue() == false {
c.Wait()
}
c.L.Unlock()
- Once でラップすることで、異なるゴルーチンで複数よばれたとしても 1 度しか実行されないようにできる
- Pool はオブジェクトプールパターンを並行処理かつ安全な形で実装したもの
- オブジェクトプールパターンは、使うものを決まった数だけプールを作る方法
- コストが高いもの(例:データベース接続)を作るときに数を制限して、決まった数しか作られないようにしつつ、予測できない数の操作がこれらへアクセスリクエストを実現するときによく使われる
3.3 チャネル(channel)
- チャネルは Hoare の CSP に由来する、Go における同期処理のプリミティブの 1 つ
- チャネルには任意の値を書き込んだり読み込んだりできる(空インタフェース型を使った場合)
- チャネルは一方向だけにデータが流れるようにも宣言できまる → 送信だけ、あるいは受信だけをするチャネルを定義できる
- 送信の場合は<-演算子をチャネルの右側に、受信の場合は<-演算子をチャネルの左側に置く
- Close でチャネルを閉じることができる。チャネルを閉じるというのは、それ以上送らないという意味であり、読み込むことは出来る
- 閉じたチャネルのパターンとして、チャネルをループで処理することで、チャネルを閉じたときに自動でループを抜け、チャネル上の値を簡潔に繰り返し取得できる
3.4 select 文
- select 文を用いることで、プログラム内でより大きな抽象化ができチャネルを組み合わせられる
3.5 GOMAXPROCS レバー
- runtime パッケージの GOMAXPROCS という関数は、OS スレッドの数を制御している
3.6 まとめ
4 章 Go での並行処理パターン
4.1 拘束
- レキシカル拘束はレキシカルスコープを使って適切なデータと並行処理のプリミティブだけを複数の並行プロセスが使えるように公開すること
- chanOwner 関数のレキシカルスコープ内で results を初期化して、consumer 関数のみで読み取るパターン
chanOwner := func() <-chan int {
results := make(chan int, 5)
go func() {
defer close(results)
for i := 0; i <= 5; i++ {
results <- i
}
}()
return results
}
consumer := func(results <-chan int) {
for result := range results {
fmt.Printf("Received: %d\n", result)
}
fmt.Println("Done receiving!")
}
results := chanOwner()
consumer(results)
4.2 for-select ループ
- for-select ループは、チャネルに対して何らかの処理をおこなるループまたはイテレーション
4.3 ゴルーチンリークを避ける
- 少ないとはいえゴルーチンもコストがかかり、またゴルーチンはランタイムによってガベージコレクションされない
- 実際の場合、ゴルーチンは長時間稼働するプログラムのはじめの方で起動され、メインゴルーチンがゴルーチンを生成し続け、最悪の場合メモリ使用率をじわじわ高めることがある
- ゴルーチンの親子間で親から子にキャンセルのシグナルを遅れるようにする
doWork := func(
done <-chan interface{},
strings <-chan string,
) <-chan interface{} {
terminated := make(chan interface{})
go func() {
defer fmt.Println("doWork exited.")
defer close(terminated)
for {
select {
case s := <-strings:
fmt.Println(s)
case <-done:
return
}
}
}()
return terminated
}
done := make(chan interface{})
terminated := doWork(done, nil)
go func() {
time.Sleep(1 * time.Second)
fmt.Println("Canceling doWork goroutine...")
close(done)
}()
<-terminated
fmt.Println("Done.")
// Canceling doWork goroutine...
// doWork exited.
// Done.
- ゴルーチンがゴルーチンの生成の責任を持っているのであれば、その語ルーチンを停止できるようにする責任もある
4.4 or チャネル
- 1 つ以上の done チャネルを 1 つの done チャネルにまとめ、どれか 1 つがとじられたらまとめて閉じる場合において、or チャネルパターンでまとめることができる
4.5 エラーハンドリング
- ゴルーチン内での処理とエラーを判断する場所を分けるなど、関心ごとを分離させる
4.6 パイプライン
- パイプラインはデータを受け取って、何らかの処理を行って、どこかに渡すという一連の作業にすぎない。これらの操作をパイプラインのステージと呼ぶ
- 単純に関数を組み合わせることもできるが、ゴルーチンを利用することで、独立して実行できるようになる
4.7 ファンアウト、ファンイン
- ファンアウトはパイプラインからの入力を扱うために複数のゴルーチンを起動するプロセスを説明する用語
- ファンインは複数の結果を 1 つのチャネルに結合するプロセスを説明する用語
4.8 or-done チャネル
- ゴルーチンでカプセル化する
4.9 tee チャネル
- tee チャネルには読み込み元のチャネルを渡し、同じ値を持つ 2 つの異なるチャネルを返す
4.10 bridge チャネル
-
チャネルのチャネルを崩して単一のチャネルにする
4.11 キュー
-
キューはプログラムを最適化する際の最後に導入すべき技術
-
デッドロックやライブロックなどといった同期に関する問題を隠してしまう
4.12 context パッケージ
-
done チャネルをこれらの情報を含めて囲む需要はどのような規模のシステムにおいても非常によくある
-
Go 1.7 で context パッケージが標準ライブラリに追加され、これを並行なコードを扱う際に考慮すべき標準的な Go のイディオムにした
-
Context 型
- 関数がランタイムにより割り込み(プリエンプション)されたときに閉じるチャンネルを返す Done メソッド
- Deadline 関数はゴルーチンが一定の時刻移行にキャンセルされるかを返す
- Err メソッドはゴル h チンがキャンセルされたら非 nil な値を返す
-
context パッケージの目的
- コールグラフの各枝をキャンセルする API 提供
- コールグラフを通じてリクエストに関するデータを渡すデータの置き場所を提供
-
Context のインタフェースには内部構造の状態を変更出来るものがなにもない
- コールスタックの上位の関数が下位の関数に対してキャンセルされてしまうことから守る
-
WithCancel は返された cancel 関数がよばれたときにその done チャネルを閉じる新しい Context を返す
-
WithDeadline はマシンの時計が与えられた deadline の時刻を経過したらその done チャネルを閉じる新しい context を返す
-
WithTimeout は与えられた timout だけ経過したらその done チャネルを閉じる新しい Context を返す