Golang並行處理和記憶體模型
go語言最好用的關鍵字:go, chan
Processes and Threads
- 程序:一個應用程式,一個為其所有資源(記憶體地址空間/裝置/執行緒)而執行的容器。
- 執行緒:一個程序從一個主執行緒開始,然後可以依次啟動更多的執行緒,執行緒共享記憶體空間。
Goroutine
1. Create a Goroutine
go關鍵字本質就是建立一個goroutine, 可以根據計算機核心來選擇並行還是併發;
main()
就是作為 goroutine 執行的。建立goroutine的例子:
func f(){ fmt.println(s) } func main(){ s := "test" go f() }
2. 銷燬Goroutine
var s string
func main(){
go func(){s = "test"}()
fmt.println(s)
}
問題點: 沒有用任何同步操作限制對s的賦值,因此其他的goroutine不一定哪呢個看到s的變化,需要用鎖或channel這種同步機制來建立程式的執行順序,即" goroutine"的並行化。
3. Goroutine核心要點
- 生命週期管理:
- 知道它什麼時候結束:
- 如何處理讓它退出:如 http.Shutdown, context delay, chan發訊號 ...
- 把並行的行為交給呼叫者:如先用函式包邏輯,在main()啟用goroutine
func main(){ go func(){ done <- serverApp() // get exit info here }() for i := 0; i < cap(done); i++{ <- done close(stop) // take it exit here } } func serverApp(stop chan struct{}) error{ // goroutine1 go func(){ <- stop http.Shutdown() }() // caller goroutine2 return http.Listen() }
並行不是併發:
- 並行:多個執行緒同時在不同的處理器執行單元執行(1個核心);
- 併發:為多個執行緒在多個核心執行。
channel
channel通訊是goroutine同步的主要方法。
每一個在特定channel的傳送操作都會匹配到通常在另一個goroutine執行的接收操作。
在channel的傳送操作先行發生於對應的接收操作完成:
var ch = make(chan int, 10)
var a string
func f(){
a = "hello, world"
ch <- 0
}
func main(){
go f()
fmt.Println(a)
}
保證"hello world"的成功print
Lock 鎖
golang的sync實現了兩個鎖的資料型別:
- sync.Mutex
- sync.RWMutex
Example: goroutine場景中,使每n次呼叫f()先行發生於第n+1次呼叫f()
var l sync.Mutex
var s string
場景:cfg為包機全域性物件,當很多goroutine同時訪問時,存在data race,會看到不連續的記憶體輸出。
用go同步語義解決:
- Mutex 互斥鎖
- RWMutex 讀寫鎖
- Atomic 原子鎖
func f() {
s = "hello world"
l.Unlock()
}
func main() {
l.Lock()
go f()
l.Lock()
fmt.Println(s)
}
Memory model
Must read reference:
闡述問題核心:Happen-Before 先行發生
多個Goroutine誰先誰後的問題, Example:
# 以下兩段程式碼,執行中若插入一個執行緒 x = 0
# 程式碼原始碼, 輸出為: 11111011111 ..
x = 0
for i in range(100):
x = 1
print(x)
# 從硬體設計看,編譯器優化重排會導致么蛾子:
x = 1
for i in range(100):
print(x)
# 輸出則可能為 11111100000 ..
- e1,e2兩個事件同時發生,沒有前後,我們認為這是併發行為
- 兩個執行緒因為記憶體重排,由cache讀取而不是到RAM,導致先行讀到0輸出。
解決問題方案:memory barrier
記憶體屏障:即"鎖"支援:
要求所有對RAM的操作必須“擴散”到memory之後才能繼續執行其他對memory的操作,我們可以用
鎖/原子/channel 或 更高階的鎖 來處理(golang標準庫可提供)。
核心概念:
memory barrier
表現為對一個變數v, "w -> r"
先寫後讀這個動作過程不受其他操作干擾:
- 讀操作
r
看到最近一次的寫操作w
寫入v
的值 - 多個Goroutine共享
v
時,必須使用同步時間來建立Happen-Before
做實驗:
go race command: go build -race xx.go
Assembly processes: go tool compile -S xx.go
package sync
場景:cfg為包機全域性物件,當很多goroutine同時訪問時,存在data race,會看到不連續的記憶體輸出。
用go同步語義解決:
- Mutex 互斥鎖
- RWMutex 讀寫鎖
- Atomic 原子鎖