大話狀態協程

引言

與用互斥鎖進行明確的鎖定來讓共享的 state 跨多個 Go 協程同步訪問, 在golang中,更多的是基於通道的方法和Go 協程間通過通訊來共享內存。如果你對此已有所瞭解,本文對你毫無意義。

數據竟爭

數據競爭,本質是多個請求對應同一共享數據的爭搶,要麼給數據上把鎖,要麼讓請求排隊,一個個上。

用鎖

好比上公共洗手間,假定只有一個坑位,多個人同時來了,都有需求。進去的人關門上鎖,外面的人等着。裏面人用完了,出來解鎖開門,下一個人接着使用。這種情況下,鎖只有一把,不一定先來的人就能上廁所。問題的關鍵在於誰獲取了鎖。用鎖,是對共享資源的保護

通信

使用內置的 Go 協程和通道的的同步特性,解決數據競爭。簡單來說,就是讓經過通道通信後,用間接方式對共享資源進行讀寫。

go通道的做法,與現實生活有相通之處。假如洗手間沒有鎖,大家都有權使用,都要用,怎麼搞?常識上大家都守規矩,先到先得(有序)可以解決。其實還有另外一種變通,像go是這樣操作,不停地通知資源的使用方,確保同一時刻只允許一個請求進來。類比面試,總有一個人站在門外通知面試者,“下一個”,然後面試者進來。仔細分析 這裏其實存在兩個問題,喊下一個這個動作是重複(意味着需要循環),另外不同的面試者,同一面試官,每次面一個,表明面試官需要不停切換面試者,用程序語言來講,共享資源(面試官)與請求(面試者)之間執行流發生了改變 。站在公司的角度,面試者需要準備好,他是被動選擇。假如洗手間會說話,用go通道來說,不是人去上洗手間,而是洗手間去通知那些等待上洗手間的人。有點控制反轉的味道。這種需要來回切換的場景,恰好適合chan通道+goroutine,通道具有天然的隊列結構,它的chan阻塞–>切換goroutine執行。

狀態協程

go狀態協程,這種通過通訊來共享內存,確保每塊數據在使用時爲單獨的 Go 協程所有。通過通信來達到共享內存,講白點,共享資源作爲私有狀態,其依據外部通信在goroutine內部進行讀寫,而對共享資源的使用依賴於chan(線程安全)通道,這就要求使用資源的對象,必須具備通信能力,對共享資源的操作,其實就是對該對象方法的操作,在方法體內可通過通道間接使用共享資源。

package main

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

// 在這個例子中,state 將被一個單獨的 Go 協程擁有。這就能夠保證數據在並行讀取時不會混亂。
//爲了對 state 進行讀取或者寫入,其他的 Go 協程將發送一條數據到擁有的 Go協程中,然後接收對應的回覆。
// 結構體 `readOp` 和 `writeOp`封裝這些請求,並且是擁有 Go 協程響應的一個方式

type readOp struct {
    key  int
    resp chan int
}
type writeOp struct {
    key  int
    val  int
    resp chan bool
}

func main() {

    //  用來計算執行操作的次數
    var ops int64

    // `reads` 和 `writes` 通道分別將被其他 Go 協程用來發布讀和寫請求。
    reads := make(chan *readOp)
    writes := make(chan *writeOp)

    // 擁有私有的 `state`  Go狀態 協程,反覆響應到達的請求。
    // 先響應到達的請求,然後返回一個值到響應通道 `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 個 Go 協程通過 `reads` 通道發起對 state 所有者Go 協程的讀取請求。
    // 每個讀取請求需要構造一個 `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.AddInt64(&ops, 1)
            }
        }()
    }

    // 用相同的方法啓動 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.AddInt64(&ops, 1)
            }
        }()
    }

    // 讓 Go 協程們跑 1s。
    time.Sleep(time.Second)

    // 最後,獲取並報告 `ops` 值。
    opsFinal := atomic.LoadInt64(&ops)
    fmt.Println("ops:", opsFinal)
}

小結

基於 Go 協程的比基於互斥鎖的稍複雜。這在某些例子中會有用,例如,在你有其他通道包含其中或者當你管理多個這樣的互斥鎖容易出錯的時候。使用最自然的方法,特別是關於程序正確性的時候。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章