1章 並行処理入門
なぜ並行性が重要になったか
だいたい2012年まではムーアの法則が成り立った
GUIベースのアプリケーションの場合はユーザーの操作という直列実行によって限界が決まる。
驚異的並列でとくべき問題は水平スケーリングで書くべき。
クラウドコンピューティングによって膨大なリソースプールからプロビジョニングしては使い捨てる。
課題はマシン同士でのやりとりや計算結果の集計・保存、どのようにコードを並行にするか
ソフトウェアがWebスケールであれば、そのソフトウェアは驚異的並列化可能な問題であると期待できる。
なぜ並行性が難しく入念な研究が必要となるか
競合状態
多くはデータ競合
アトミック性
アトミック性があると考えられる場合、それが操作されている特定のコンテキストの中では分割不能あるいは中断不可であることを意味する。
メモリアクセス同期
以下のコードはデータ競合でプログラムの出力は非決定的である。
code:main.go
var data int
go func() { data++ }()
if data == 0 {
fmt.Println("the value is 0.")
else {
fmt.Printf("the value is %v.\n", data)
}
プログラム内で共有リソースに対する排他的なアクセスが必要な場所をクリティカルセクションと呼ぶ。
data変数をインクリメントするゴルーチン
dataの値が0かを確認しているif文
dataの値をとってきて出力しているfmt.Printf文
クリティカルセクション間でのメモリへのアクセスを同期することで、プログラムのクリティカルセクションを守ることができる。
以下のコードは理想的ではないがメモリアクセス同期を簡潔に表している。
慣例的にdata変数のメモリにアクセスしたい時は初めにLockを呼び、アクセスする処理が終わったらUnlockを呼ぶ。
プログラムへの操作の順序は非決定的である。
ゴルーチンが先かifとelseのブロックが先か
慣例は容易に無視されるものである。
code:main.go
var memoryAccess sync.Mutex // 1. メモリへのアクセスを同期的にする。
var data int
go func() {
memoryAccess.Lock() // 2. ゴルーチンはそのメモリに対する排他的アクセスを取得して、解放するまで続く。
data++
memoryAccess.UnLock() // 3. ゴルーチンがメモリの排他的アクセスを解放する。
}()
memoryAccess.Lock() // 4. ここでまた生魚分がdata変数のメモリに対して排他的アクセスを取得できるように宣言する。
if data == 0 {
fmt.Printfln("the value is 0.\n")
} else {
fmt.Printf("the value is %v.\n", data)
}
memoryAccess.UnLock() // 5. 再度ここでこのメモリに対する処理が終わったことを宣言する。
デッドロック、ライブロック、リソース枯渇
デッドロック
全ての並行なプロセスがお互いの処理を待ち合っている状況のこと
code: main.go
type value struct {
mu sync.Mutex
value int
}
var wg sync.WaitGroup
printSum := func(v1, v2 *value) {
defer wg.Done()
v1.mu.Lock() // 1. クリティカルセクションに入る。
defer v1.mu.Unlock()
time.Sleep(2 * time.Second) // 2.
v2.mu.Lock()
defer v2.mu.Unlock()
fmt.Printf("sum=%v\n", v1.value+v2.value)
}
var a, b value
wg.Add(2)
go printSum(&a, &b)
go printSum(&b, &a)
wg.Wait()
デッドロックが発生するために必要な条件Coffman条件と言う。
相互排他
ある並行プロセスがリソースに対して排他的な権利をどの時点においても保持している。
条件待ち
ある並行プロセスはリソースの保持と追加のリソース待ちを同時に行わなければならない。
横取り不可
ある並行プロセスによって保持されているリソースは、そのプロセスによってのみ解放される。
循環待ち
ある並行プロセス(P1)は、他の連なっている並行プロセス(P2)を待たなければならない。
上記のコードは
printSum関数はaとbの両方に対して排他的アクセス権が必要なので相互排他を満たす。
printSum関数はaもしくはbのどちらかを保持していて、もう片方を待っているので、この条件待ちを満たしている。
ゴルーチンを横取りする方法は提供されていないため横取り不可は満たしている。
printSumの最初の呼び出しでは2番目の呼び出しを待っていて、逆も然りなので循環待ちを満たしている。
ライブロック
並行操作をおこなっているけど、その操作はプログラムの状態を全く進めていないプログラムを指す。
リソース枯渇
なぜGoは並行処理を早く綺麗に書けるのか