1. 程式人生 > 實用技巧 >Golang並行處理和記憶體模型

Golang並行處理和記憶體模型

GitHub_Note: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核心要點

  1. 生命週期管理:
  • 知道它什麼時候結束:
  • 如何處理讓它退出:如 http.Shutdown, context delay, chan發訊號 ...
  1. 把並行的行為交給呼叫者:如先用函式包邏輯,在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的傳送操作先行發生於對應的接收操作完成:

sync_channel.go

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 鎖

https://golang.org/pkg/sync/

golang的sync實現了兩個鎖的資料型別:

  • sync.Mutex
  • sync.RWMutex

Example: goroutine場景中,使每n次呼叫f()先行發生於第n+1次呼叫f()

sync_mutex.go

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 ..

  1. e1,e2兩個事件同時發生,沒有前後,我們認為這是併發行為
  2. 兩個執行緒因為記憶體重排,由cache讀取而不是到RAM,導致先行讀到0輸出。

解決問題方案:memory barrier 記憶體屏障:即"鎖"支援:

要求所有對RAM的操作必須“擴散”到memory之後才能繼續執行其他對memory的操作,我們可以用
鎖/原子/channel 或 更高階的鎖 來處理(golang標準庫可提供)。

核心概念:

memory barrier 表現為對一個變數v, "w -> r" 先寫後讀這個動作過程不受其他操作干擾:

  1. 讀操作r看到最近一次的寫操作w寫入v的值
  2. 多個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 原子鎖