# Chapter 9. Concurrency with Shared Variables ## Reference https://notes.shichao.io/gopl/ch9/ https://github.com/sean1093/golang-study-group/blob/master/Notes/Chapter%209%20%E5%85%B1%E7%94%A8%E8%AE%8A%E6%95%B8%E4%B8%A6%E8%A1%8C%E6%80%A7.md ## 一句話總結此單元 讓程式高效率且正確地執行 ## 前情提要 ### sequential program 只有一個 goroutine 的程式,會按照程式碼邏輯的順序執行 ### concurrent(交錯 在有兩個以上 goroutine 的程式裡 x, y 兩個 event 分別在不同的 goroutine 中 當我們沒辦法確定 x, y 誰會先執行的時候 就可以說 x, y 兩個 event 是 concurrent ### concurrency-safe 交錯執行的時候不會影響結果 #### concurrency-safe function 當一個 function 陸陸續續被不同的 goroutine 呼叫 還不會造成執行錯誤 就稱它是一個 concurrency-safe function 例: ```go= package main import ( "fmt" "math" "time" ) func concurrentSafe(f float64) { for i := 0; i < 5; i++ { time.Sleep(100 * time.Millisecond) fmt.Println(f) } } func main() { go concurrentSafe(math.Abs(-42)) concurrentSafe(math.Abs(-4)) } } ``` 在輸出結果可以看到 `concurrentSafe(math.Abs(-42))`跑在另一個 goroutine 上跟 `concurrentSafe(math.Abs(-4))`交錯執行 但依然是正常輸出結果,這是因為 `math.Abs(-4)` 是 `exported package-level functions` ``` >> go run main.go 42 4 4 42 42 4 4 42 42 4 ``` #### concurrency-safe type 所有 accessible methods 都是 concurrency-safe #### concurrency-safe program 禁止 concurrent access 所有變數,除了 concurrency-safe type。 具體執行方式有兩個 1. 限制變數只在同一個 goroutine 被 access 2. 維護一個每次都只允許單一存取的不會被執行順序亂衝康的值 原文:Maintaining a higher-level invariant of mutual exclusion ## Race Conditions 程式在多個 goroutine 交錯執行後,輸出錯誤結果 通常發生在資料量很大的時候,所以很難重新模擬出同樣的錯誤情境來偵錯 ### data race data race 是一種 Race Condition 發生在多個 goroutine 對同一個變數交錯存取時並且至少有一個存取是寫入的時候 #### primitive data type: int ```go= // Package bank implements a bank with only one account. package bank var balance int func Deposit(amount int) { balance = balance + amount } func Balance() int { return balance } ``` ```go= // Alice: go func() { bank.Deposit(200) // A1 fmt.Println("=", bank.Balance()) // A2 }() // Bob: go bank.Deposit(100) // B ``` 現在有兩個 goroutine 三個 event: A1, A2, B 執行順序導致不同的結果 ``` A1 -> A2 -> B >> =200 B -> A1 -> A2 >> =300 A1 -> B -> A2 >> =300 ``` #### composite data type: slice ```go= var x []int go func() { x = make([]int, 10) }() go func() { x = make([]int, 1000000) }() x[999999] = 1 // NOTE: undefined behavior; memory corruption possible ``` slice 由 pointer, length, capacity 組成 所以在交錯執行的時候有可能發生以下情形 pointer 指到第二行make出的length=10的記憶體位置 length被第三行定義成1000000 導致第四行的`x[999999]`其實指向沒被定義的記憶體位置 ### Avoiding a data race 有三種方式可以預防 data race #### Avoid writing the variable 這個程式會在第一次 call 到`Icon(name)`才會去將指定 `name` 的 `icon` 寫進 `icons`(map) 當同時有多個 goroutine 在 call 時,`icons`(map)就會發生 data race ```go= var icons = make(map[string]image.Image) func loadIcon(name string) image.Image // NOTE: not concurrency-safe! func Icon(name string) image.Image { icon, ok := icons[name] if !ok { icon = loadIcon(name) icons[name] = icon } return icon } ``` 所以如果先把 `icons`(map) 定義好 每次 call `Icon(name)`時就只是讀取的動作 預防了 data race ```go= var icons = map[string]image.Image{ "spades.png": loadIcon("spades.png"), "hearts.png": loadIcon("hearts.png"), "diamonds.png": loadIcon("diamonds.png"), "clubs.png": loadIcon("clubs.png"), } // Concurrency-safe. func Icon(name string) image.Image { return icons[name] } ``` #### Avoid accessing the variable from multiple goroutines > Go mantra: > "Do not communicate by sharing memory; instead, share memory by communicating." 在這邊我們把 `balances` 綁定在 `teller` 這個 monitor goroutine 代表我們只能透過 `teller` 裡面的 channel 來去存取 `balances` ```go= // Package bank provides a concurrency-safe bank with one account. package bank var deposits = make(chan int) // send amount to deposit var balances = make(chan int) // receive balance func Deposit(amount int) { deposits <- amount } func Balance() int { return <-balances } func teller() { var balance int // balance is confined to teller goroutine for { select { case amount := <-deposits: balance += amount case balances <- balance: } } } func init() { go teller() // start the monitor goroutine } ``` 在這個例子 Cake 則是先被綁定在 baker 然後在被綁定在 icer ```go= type Cake struct{ state string } func baker(cooked chan<- *Cake) { for { cake := new(Cake) cake.state = "cooked" cooked <- cake // baker never touches this cake again } } func icer(iced chan<- *Cake, cooked <-chan *Cake) { for cake := range cooked { cake.state = "iced" iced <- cake // icer never touches this cake again } } ``` #### Allow only one goroutine to access the variable at a time 就是我們接下來要講的 Mutual exclusion ## Mutual Exclusion: sync.Mutex 不太懂`struct{}{}`是啥 但這是 binary semaphone ```go= var ( sema = make(chan struct{}, 1) // a binary semaphore guarding balance balance int ) func Deposit(amount int) { sema <- struct{}{} // acquire token balance = balance + amount <-sema // release token } func Balance() int { sema <- struct{}{} // acquire token b := balance <-sema // release token return b } ``` sync.Mutex 藉由下面的方式來實作 Mutual Exclusion * `Lock()`來得到`lock`這個token * `UnLock()`釋放`lock` * 有`lock`才能進入 critical section ```go = import "sync" var ( mu sync.Mutex // guards balance balance int ) func Deposit(amount int) { mu.Lock() balance = balance + amount // critical section mu.Unlock() } func Balance() int { mu.Lock() b := balance // critical section mu.Unlock() return b } ``` 這邊用`defer`就能確保程式就算遇到panic也會執行`Unlock()` ```go= func Balance() int { mu.Lock() defer mu.Unlock() return balance // critical section } ``` ## Read/Write Mutexes: sync.RWMutex * 適合讀取的次數遠大於寫入的次數的情境 * 在寫入的時候會禁止讀寫,但讀的時候不會限制其他人讀 * 相較`sync.Mutex`來說比較耗資源 ```go= var mu sync.RWMutex var balance int func Balance() int { mu.RLock() // readers lock defer mu.RUnlock() return balance } ``` ## Memory Synchronization [ref](https://github.com/sean1093/golang-study-group/blob/master/Notes/Chapter%209%20%E5%85%B1%E7%94%A8%E8%AE%8A%E6%95%B8%E4%B8%A6%E8%A1%8C%E6%80%A7.md#94-%E8%A8%98%E6%86%B6%E9%AB%94%E5%90%8C%E6%AD%A5) > 多處理器的架構下, 各個處理器有自己的cache, cache不一定會更新到最新狀態, 不同的編輯器或CPU會有不同的行為 > 用mutex或是channel可以確保資料被更新到各個cache裡 ## Lazy Initialization: sync.Once `sync.Once`裡面含 mutex 跟一個 bool * mutex: 讓初始化的過程只有一個 go routine 在做 * bool: 確認初始化只做過一次 ```go= var loadIconsOnce sync.Once var icons map[string]image.Image func loadIcons() { icons = make(map[string]image.Image) icons["spades.png"] = loadIcon("spades.png") icons["hearts.png"] = loadIcon("hearts.png") icons["diamonds.png"] = loadIcon("diamonds.png") icons["clubs.png"] = loadIcon("clubs.png") } // Concurrency-safe. func Icon(name string) image.Image { loadIconsOnce.Do(loadIcons) return icons[name] } ``` ## The Race Detector go 提供動態分析的工具,來偵測 data race 的情形 用法:可以在 `go run` `go test`後面加上`-race`的 flag ``` go run -race main.go ``` ## Example: Concurrent Non-Blocking Cache(真看不懂來日方長) ## Goroutines and Threads ### OS thread v.s. Goroutine | Name | OS thread | Goroutine | | ------------------ | ------------------- | ---------------------- | | stack size | fixed, usually 2MB | scalable, 2KB ~ 1GB | | schedule by | kernel | Go scheduler | | invoke schedule by | hardware timer | sleep, block, or mutex | | how to schedule | full context switch | put it to sleep | ### Goroutine: m:n scheduling m Goroutine on n OS threads ### Goroutine: GOMAXPROCS how many OS threads may be executing Go code simultaneously. (# CPUs) Example: single OS thread: $ GOMAXPROCS=1 go run hacker-cliché.go 111111111111111111110000000000000000000011111... two OS threads:(幾乎是同時交互進行) $ GOMAXPROCS=2 go run hacker-cliché.go 010101010101010101011001100101011010010100110... ### Goroutines have no Identity 故意的XD不想讓妳亂用