前回の例では、複数のゴルーチンから共有状態への アクセスを同期するために、ミューテックス による明示的なロックを使いました。 同じ結果を得るために、ゴルーチンとチャネルがもつ 組み込みの同期機能を使う方法もあります。 チャネルベースのアプローチは、通信によってメモリを共有し、 各データは 1 つのゴルーチンによって所有されるべきという、 Go の考え方に沿っています。 |
|
package main |
|
import ( "fmt" "math/rand" "sync/atomic" "time" ) |
|
この例では、状態は 1 つのゴルーチンに所有させます。
これは、データが並行アクセスで決して壊れないことを保証します。
ほかのゴルーチンが状態を読み書きするためには、
データを所有するゴルーチンにメッセージを送り、
対応するデータを受け取る必要があります。
|
type readOp struct { key int resp chan int } type writeOp struct { key int val int resp chan bool } |
func main() { |
|
前回と同様に、操作回数をカウントします。 |
var readOps uint64 var writeOps uint64 |
|
reads := make(chan readOp) writes := make(chan writeOp) |
これは前回の例と同様にマップ |
go func() { var state = make(map[int]int) for { select { case read := <-reads: read.resp <- state[read.key] case write := <-writes: state[write.key] = write.val write.resp <- true } } }() |
ここで、状態をもつゴルーチンから |
for r := 0; r < 100; r++ { go func() { for { read := readOp{ key: rand.Intn(5), resp: make(chan int)} reads <- read <-read.resp atomic.AddUint64(&readOps, 1) time.Sleep(time.Millisecond) } }() } |
同様に、書き込み用のゴルーチンを 10 個開始します。 |
for w := 0; w < 10; w++ { go func() { for { write := writeOp{ key: rand.Intn(5), val: rand.Intn(100), resp: make(chan bool)} writes <- write <-write.resp atomic.AddUint64(&writeOps, 1) time.Sleep(time.Millisecond) } }() } |
ゴルーチンを 1 秒間動作させます。 |
time.Sleep(time.Second) |
最後に、操作回数を取得してレポートします。 |
readOpsFinal := atomic.LoadUint64(&readOps) fmt.Println("readOps:", readOpsFinal) writeOpsFinal := atomic.LoadUint64(&writeOps) fmt.Println("writeOps:", writeOpsFinal) } |
プログラムを実行すると、ゴルーチンベースの状態管理で 80,000 回程の操作を完了したことが分かります。 |
$ go run stateful-goroutines.go readOps: 71708 writeOps: 7177 |
今回の例では、ミューテックスベースと比べてゴルーチンベースの アプローチはやや複雑でした。 しかし、他の関連するチャネルがある場合などは、 複数のミューテックスを管理するのは間違えやすいので、 こちらのアプローチが有用です。 いずれにせよ、正しいプログラムを作るのに最も適切だと 思えるアプローチを使うべきです。 |
Next example: Sorting.