引言
與用互斥鎖進行明確的鎖定來讓共享的 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 協程的比基於互斥鎖的稍複雜。這在某些例子中會有用,例如,在你有其他通道包含其中或者當你管理多個這樣的互斥鎖容易出錯的時候。使用最自然的方法,特別是關於程式正確性的時候。
本作品採用《CC 協議》,轉載必須註明作者和本文連結