例から学ぶ Go: 有状態のgoroutine

前の例では、共有ステートへのアクセスを複数のgoroutineから同期する為に、明示的なロックにミューテックスを利用しました。その他の選択肢として、goroutineとチャネルに組み込まれている同期機能を使用して同じ結果を得ることもできます。このチャネルベースのアプローチでは、メモリを共有する方法を決定します。各データは、1つのgoroutineによって所有されます。

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

この例では、1つのgoroutineがステートを所有します。これにより、データは同時にアクセスされて破損することは決してありません。他のgoroutineは、ステートを読み書きするために、所有するgoroutineにメッセージを送信し、対応する応答を受信します。これらのreadOpwriteOp構造体は、これらの要求と所有するgoroutineが応答する方法をカプセル化します。

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

チャネルreadswritesは、他のgoroutineによって、読み取りと書き込みのリクエストを実行するために使用されます。

    reads := make(chan readOp)
    writes := make(chan writeOp)

ここでは、goroutineがstateを所有しています。これは、前の例と同様のmapですが、現在は有状態のgoroutine専用です。このgoroutineは、リクエストが到着するとそれに応答して、readswritesチャネルで繰り返し選択します。応答は、最初に要求された操作を実行してから、応答チャネル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のgoroutineを開始して、readsチャネルを介して状態を所有するgoroutineに読み取りを実行します。各読み取りには、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)
            }
        }()
    }

goroutineを1秒間動作させます。

    time.Sleep(time.Second)

最後に、opのカウントをキャプチャしてレポートします。

    readOpsFinal := atomic.LoadUint64(&readOps)
    fmt.Println("readOps:", readOpsFinal)
    writeOpsFinal := atomic.LoadUint64(&writeOps)
    fmt.Println("writeOps:", writeOpsFinal)
}

プログラムを実行すると、goroutineベースのステート管理の例では合計約80,000の操作が完了します。

$ go run stateful-goroutines.go
readOps: 71708
writeOps: 7177

この特定の場合、goroutineベースのアプローチはミューテックスベースのアプローチよりも少し複雑でした。ただし、他のチャネルが関与する場合や、そのような複数のミューテックスを管理する場合など、特定のケースでは役立つ可能性があります。特にプログラムの正しさの観点から、最も自然に感じられるアプローチを使用する必要があります。

次の例: ソート