Go 範例:有狀態 goroutine

在先前的範例中,我們使用明確的鎖定 互斥鎖,以同步多個 goroutine 存取共用狀態。另一種選擇是使用 goroutine 和通道的內建同步功能來達成相同的結果。這種基於通道的方式符合 Go 的記憶體分享觀念,透過溝通來分享記憶體,並且每一份資料都正由一個 goroutine 擁有著。

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

在此範例中,我們的狀態將由一個 goroutine 擁有。這將保證資料不會在同時存取時損毀。若要讀取或寫入該狀態,其他 goroutine 會將訊息傳送給擁有的 goroutine,並收到對應的回覆。這些 readOpwriteOp struct 封裝了這些請求,以及擁有 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)

這是擁有 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 執行一秒。

    time.Sleep(time.Second)

最後,擷取並回報操作次數。

    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 的方式會比基於互斥鎖的方式複雜一點。但在某些狀況下它可能會很有用,例如處理其他相關通道的狀況,或管理多個互斥鎖容易出錯時。您應使用覺得最直覺的方式,特別是在了解程式的正確性的方面。

下一個範例:排序