GoLang併發控制(上)
GoLang併發控制(上)
在go程式中,最被人所熟知的便是併發特性,一方面有goroutine這類二級執行緒,對這種不處於使用者態的go程的支援,另一方面便是對併發程式設計的簡便化,可以快捷穩定的寫出支援併發的程式。
-
先回顧程序or執行緒之間的通訊方式
inte-process communication(IPC)
其中Go支援的IPC方法有管道、訊號和socket。篇(shui)幅(ping)有限,一張圖引入回憶。
ipc圖解.jpg
-
併發和並行
簡單來講 併發就是可同時發起執行的程式,並行就是可以在支援並行的硬體上執行的併發程式;換句話說,併發程式代表了所有可以實現併發行為的程式,這是一個比較寬泛的概念,並行程式也只是他的一個子集。 這個問題在知乎筆試中出現過,並行和併發不是一個概念。
複習過過去的基礎知識後,進入主題。
開發go程式,不管是系統性的k8s平臺,還是基於傳統的web開發,都常常使用goroutine來併發處理任務,有時候goroutine之間是相互獨立的,但是也有時候goroutine之間是需要同步和通訊的;另一種情況是父goroutine需要控制屬於他的子goroutine。而goroutine的設計機制為,goroutine退出只能由本身進行控制,不同與傳統的使用者態協程,不允許從外部強制結束該goroutine,除非goroutine奔潰或者main函式結束。目前實現多個goroutine間的同步與通訊大致有:
- 全域性共享變數
- channel管道通訊---CSP模型
- context包---在1.7版本後引入
全域性共享變數:
實現思路為:
- 申明一個全域性變數。
- 在這個全域性變數作用域中,開啟多個go程,多個go程實際上是共享這個全域性變數。
- 開啟的多個go程實現迴圈,不斷的監聽這個全域性變數的情況,若全域性變數屬性觸發一定條件,跳出迴圈,按序列順序執行到該go程結尾,go程生命週期結束。
程式碼示例:
package main import ( "fmt" "time" ) func main() { running := true f := func() { for running { //控制的全域性共享變數 fmt.Println("sub proc running...") time.Sleep(1 * time.Second) } fmt.Println("sub proc exit") } go f() go f() go f() time.Sleep(2 * time.Second) running = false //全域性共享變數改變 time.Sleep(3 * time.Second) fmt.Println("main proc exit") }
- 優點:實現簡單,不抽象,直白方便,一個變數即可簡單控制子goroutine的進行。
-
缺點:
- 不能適應結構複雜的設計,功能有限,只能適用於子go程中讀,外主程或父go程來寫全域性變數,若子go程中進行寫,會出現資料同步問題,需要加鎖解決,不加鎖面對map這類執行緒不安全的結構會報錯。
- 還有不適合用於同級的子go程間的通訊,全域性變數傳遞的資訊太少。
- 還有就是主程序無法等待所有子goroutine退出,因為這種方式只能是單向通知,所以這種方法只適用於非常簡單的邏輯且併發量不太大的場景。
channel通訊
首先解釋golang中的channel:channel是go中的核心部分之一,結構體簡單概括就是一個ring佇列+一個鎖 有興趣的同學可以去研究一下原始碼構建。在使用中可以將channel看做管道,通過channel迸發執行的go程之間就可以傳送或者接受資料,從而對併發邏輯進行控制。
go的channel的設計是建立在CSP(Communicating Sequential Process),中文可以叫做通訊順序程序,是一種併發程式設計模型,由 Tony Hoare 於 1977 年提出。簡單來說,CSP 模型由併發執行的實體(執行緒或者程序)所組成,實體之間通過傳送訊息進行通訊,這裡傳送訊息時使用的就是通道,或者叫 channel。CSP 模型的關鍵是關注 channel,而不關注傳送訊息的實體。Go 語言實現了 CSP 部分理論,goroutine 對應 CSP 中併發執行的實體,channel 也就對應著 CSP 中的 channel。 也就是說,CSP 描述這樣一種併發模型:多個Process 使用一個 Channel 進行通訊, 這個 Channel 連結的 Process 通常是匿名的,訊息傳遞通常是同步的(有別於 Actor Model)。
設計思路:
- 建立一個sync包中WaitGroup例項 var wg sync.WaitGroup
- 建立一個chan,負責控制go程退出
- 在每一個go程被建立前,執行註冊. wg.Add(1)
- 建立go後,在go程中監聽訊號chan能否收到,使用select機制(和io多路複用相似)
- runtime主程 直接關閉chan,也可以選擇傳送訊號量。通知子go程結束迴圈,結束go程
- go程呼叫 wg.Done()登出後再退出,所以在進行go程 使用defer,go程退出執行。
- wg.Wait() 在註冊的所有信息登出後才繼續執行下一步。原理是實現一個for死迴圈,在註冊的值消耗完畢後,跳出死迴圈。
程式碼解釋:
package main import ( "fmt" "os" "os/signal" "sync" "syscall" "time" ) //執行的go程方法,傳入的chan func consumer(stop <-chan bool) { for { select {//機制類似epoll case <-stop: fmt.Println("exit sub goroutine") return default: fmt.Println("running...") time.Sleep(500 * time.Millisecond) } } } func main() { stop := make(chan bool) var wg sync.WaitGroup // Spawn example consumers for i := 0; i < 3; i++ { wg.Add(1) go func(stop <-chan bool) { defer wg.Done() consumer(stop) }(stop) } close(stop) //直接關閉chan,否則傳遞3次訊號量 fmt.Println("stopping all jobs!") wg.Wait() fmt.Println("OVER") }
channel通訊控制基於CSP模型,相比於傳統的執行緒與鎖併發模型,避免了大量的加鎖解鎖的效能消耗,而又比Actor模型更加靈活,使用Actor模型時,負責通訊的媒介與執行單元是緊耦合的–每個Actor都有一個信箱。而使用CSP模型,channel是第一物件,可以被獨立地建立,寫入和讀出資料,更容易進行擴充套件。