Stateful Goroutines

Stateful Goroutines #

// [mutex](mutexes.html) を使い明示的なロックを取って、複数のゴルーチンで共有データへのアクセスを同期する方法は既に紹介した。
// ゴルーチンを同期する組み込みの機能とチャネルを使っても、同じ結果が得られる。
// チャネルを使うこのやり方は、各ゴルーチンが持つデータをやり取りしてメモリを共有する Go のアイデアに合っている。

package main

import (
	"fmt"
	"math/rand"
	"sync/atomic"
	"time"
)

// この例では、あるゴルーチンだけが状態を所有する。
// この結果、アクセスの競合によってデータが壊れてしまうことなくなる。
// 状態を読み書きするために、他のゴルーチンは状態を持つゴルーチンにメッセージを送り、返信を受け取る。
// 構造体 `readOp`、`writeOp` はこれらのリクエストをカプセル化したものだ。
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`、`writes` を使って、他のゴルーチンは読み書きのリクエストする。
	reads := make(chan readOp)
	writes := make(chan writeOp)

	// このゴルーチンが `state` を所有する。
	// `state` は前の例と同様のマップだが、この状態管理用のゴルーチンだけが読み書きをする。
	// このゴルーチンはチャネル `reads`、`writes` を繰り返し `select` し、届いたリクエストに返信する。
	// リクエストを受け取ると、リスエストされた操作を実行し、値を返信用のチャネル `resp` に送信し、リクエストが成功したことを伝える(`reads` の場合は読み出した結果も伝える)。
	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
			}
		}
	}()

	// ここで100個のゴルーチンを開始し、チャネル `reads` を通じて状態を管理するゴルーチンに読み出しを発行する。
	// 読み出しリクエストのたびに `readOp` を作り、チャネル `reads` にそれを送り、チャネル `resp` から結果を受け取る。
	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)
}
# プログラムを実行するとゴルーチンを使った状態管理の例では約80000回の操作を実行できたことがわかる。
$ go run stateful-goroutines.go
readOps: 71708
writeOps: 7177

# この例ではゴルーチンを使ったやり方はミューテックスを使う場合と比べて少し処理が多かった。
# しかし、場合によってはこのやり方の方が便利なこともある。
# 例えば、他にもチャネルを使っている場合や、複数のミューテックスを使った結果プログラムを間違えそうな場合である。
# プログラムの正しさがパッと見てわかるような、自然なやり方を採用するのがよい。