拘束
情報(データ)を、確実に1つの並行プロセスからのみ得られるようにすること。
これが実現できると
- プログラマーがデータの中身を意識する負荷が下がる
- クリティカルセクションが小さくなる
- 並行プログラムが安全で、同期不要なものになる
拘束には次の2パターンある。
- アドホック拘束
- チーム内の規約(ルール)によって拘束する。例えば、「変数
A
は関数B
からしかアクセスしないようにしよう」とイメージ。 - 規約を守り続けるのは難しい
- チーム内の規約(ルール)によって拘束する。例えば、「変数
- レキシカル拘束
- レキシカルスコープを使って適切な範囲でデータを公開する。
アドホック拘束
アドホック拘束のサンプルコード。
package main import "fmt" func main() { // dataはどこからでもアクセスできるが、loopData関数内のみからアクセスするように書かれている data := []int{10, 20, 30} // 送信専用チャネルを引数として受け取り // dataの値を1つずつチャネルに送信する loopData := func(ch chan<- int) { defer close(ch) for i := range data { ch <- data[i] } } ch := make(chan int) go loopData(ch) // チャネルから受信した値を出力 for v := range ch { fmt.Println(v) } // 実行結果 // 10 // 20 // 30 }
コメントに書いた通り、変数 data
にはどこかれでもアクセスできるが、loopData
関数からのみアクセスするように書かれている。この規約(ルール)がアドホック拘束。
しかし、開発を続けているうちに、この規約が破られて問題が起こる可能性が高い。
レキシカル拘束
レキシカル拘束のサンプル。
package main import "fmt" func main() { chanOwner := func() <-chan int { // 関数内でチャネルを初期化して、受信操作専用のチャネルを返す // こうすることでチャネルへ書き込みができるスコープを制限できる resultCh := make(chan int) go func() { defer close(resultCh) for i := 0; i < 3; i++ { resultCh <- i } }() return resultCh } // 受信操作専用のチャネルを受け取り、値を出力していく printer := func(resultCh <-chan int) { for v := range resultCh { fmt.Println(v) } fmt.Println("Done!!") } resultCh := chanOwner() printer(resultCh) // 実行結果 // 0 // 1 // 2 // Done!! }
chanOwner
関数のレキシカルスコープ内で resultCh
チャネルを初期化して、受信操作専用のチャネルとして返している。こうすることで、resultCh
チャネルへ送信できるのは chanOwner
関数のみになる。
つまり、resultCh
チャネルへの送信権限を拘束して、他のゴルーチンが誤った値を送信するのを防いでいる。
※ レキシカル拘束は、別にチャネルに限った話ではない。変数全般に対して言えること。レキシカルスコープを使って適切な範囲にのみ公開するように心がけると安全な処理が書ける。(特に並行処理では重要)
レキシカル拘束の例をもう一つ。
package main import ( "fmt" "sync" ) func main() { sumPrinter := func(wg *sync.WaitGroup, data []int) { defer wg.Done() var sum int for _, v := range data { sum += v } fmt.Printf("sum = %d\n", sum) } var wg sync.WaitGroup data := []int{1, 2, 3, 10, 20, 30} wg.Add(2) go sumPrinter(&wg, data[3:]) // => wum = 6 go sumPrinter(&wg, data[:3]) // => sum = 60 wg.Wait() fmt.Println("Done!!") }
この例では、
sumPrinter
関数はdata
スライスの宣言前に定義されているので直接アクセスできず、引数として受け取っている。- 呼び出し側のゴルーチンでは、
data
スライスの部分集合を渡しているので、sumPrinter
関数はスライスの一部しかアクセスできない
→ data
スライスへのアクセス範囲を拘束している。
参考図書
Tour of Goを終えた人にオススメです!!