Go語言併發程式設計簡單入門

abcjob發表於2021-09-09

併發是邏輯上具備同時處理多個任務的能力,並行是在物理上的同一時刻執行多個併發任務。在單核處理器上,它們可以使用間隔的方式切換執行,並行則是依賴多核處理器的物理裝置的特性。

平行計算是併發設計的最理想模式。

多執行緒或者多程式是並行的基本條件,但是單執行緒也可以用協程做到併發。儘管協程在單執行緒上透過主動切換來實現多工併發,但它也有自己的優勢。協程上執行的多個任務本質上是序列執行的,加上可控自主排程,所以並不需要做同步處理。

即使採用多執行緒也未必就能執行並行。Python就因為GIL限制,預設只能併發而不能並行,所以很多時候轉而使用"多程式+協程"架構。

通常情況下,用多執行緒來實現分散式和負載均衡,減輕單程式垃圾回收壓力,用多程式(LWP)搶奪更多的處理器資源,用協程來提高處理器時間片利用率。

關鍵字go並非執行併發操作,而是建立一個併發任務單元。新建任務唄放置到系統佇列中,等待排程器安排合適系統執行緒去獲取執行權。當前流程不會阻塞,不會等待該任務啟動,去執行時也不保證併發任務的執行次序。

每個任務單元儲存除了函式指標、呼叫引數外,還會分配執行所需的棧記憶體空間。相比系統預設的KB級別的執行緒棧,goroutine自定義棧僅僅需要初始化2KB,所以才可以建立成千上萬的併發任務。自定義棧採取按需分配策略,在需要的時候進行擴容,最大能到GB規模。

Wait函式:程式退出時不會等待併發任務結束,可以使用通道阻塞,然後發出退出訊號。

除了關閉通道以外,寫入資料也可以接觸阻塞。

如果要等待多個任務結束,推薦使用sync.WaitGroup。透過設定計數器讓每個goroutine在退出前遞減,直到遞迴為0時解除阻塞。儘管WaitGroup.Add函式實現了原子操作,但是建議在goroutine外累加計數器,以避免Add尚未執行,Wait已經退出。

GOMAXPROCS:執行時可能會建立多個執行緒,但是任何時候僅僅有有限的執行緒參與併發任務的執行,這個數量和處理器的核心數是相等的。可以使用runtime.GOMAXPROCS函式修改,也可以使用環境變數。

如果引數是小於1的,GOMAXPROCS僅僅返回當前設定的值,不做任何調整。

可以使用runtime.NumCPU來顯示CPU的核心數。

LocalStorage:gorontine任務無法設定優先順序,無法獲取編號,沒有區域性儲存(TLS),甚至連返回值都會被拋棄。如果使用map作為區域性儲存器,建議期間做同步處理,因為執行時會對其進行併發讀寫檢查。

Gosched:暫停,釋放執行緒去執行其他任務。當前任務被放回佇列,等待下次排程是恢復執行。該函式很少被使用,因為執行時會主動像長時間執行(10ms)的任務發出搶佔排程。只是當前版本實現演算法的問題,不能保證排程總是成功的,所以主動切換還有使用場合。

Goexit:立即終止當前任務,執行時確保所有已經註冊延遲呼叫被執行。該函式不會影響其他併發任務,不會引起panic,自然也就無法捕獲。

如果在main.main裡呼叫Goexit,它會等待其他任務結束,然後讓其他程式直接崩潰。

無論在那一層,Goexit都可以立即終止整個呼叫棧,與return不同,標準庫函式os.exit可以終止程式,但是不會執行延遲呼叫。

通道:

Go並未實現嚴格的併發安全。

Go鼓勵使用CSP通道,使用通訊來代替記憶體共享,實現併發安全。

透過訊息來避免競態的模型除了CSP,還有Actor。

作為CSP的核心,通道是顯式的,要求操作的雙方必須知道資料型別和具體的通道,並不關心另一端操作者的身份和數量。可如果另一端為準備妥當,或者訊息未能及時處理,會阻塞當前端。

Actor是透明的,不在乎資料型別及通道,只要知道接受者的信箱就行,預設是非同步的方式。

通道只是一個佇列。同步模式下,傳送和接收方配對,然後直接複製資料給對方。如果配對失敗,就會置入等待佇列,直到另一方出現後才會被喚醒。

非同步模式搶奪的是資料緩衝槽。傳送方要求有空槽可供寫入,而接收方就會要求緩衝資料可以讀取。需求不符合的時候同樣加入到等待的佇列,直到另一方寫入資料或者是騰出空的資料緩衝槽之後才會被喚醒。

通道還會被用作事件通知。

同步模式下必須有配對操作的goroutine操作出現,否則會一直阻塞。

多數時候,非同步通道有助於提升功能,減少排隊阻塞。

雖然傳遞指標可以來避免資料的複製,但是必須注意額外的資料併發的安全性。

內建函式cap和len返回緩衝器大小和當前已經緩衝的數量,而對於同步通道則都會返回0,可以根據這個特徵判斷通道是同步的還是非同步的。

可以使用ok-idom或者是range模式進行處理資料。對於迴圈接收資料range更加簡潔一些。及時使用close函式關閉通道引發結束結束通知,否則可能會導致死鎖。

通知可以是群體型別的。一次性事件使用close效率會更好一些,沒有多餘的開銷。連續或多樣性事件,可以傳遞不同資料標識實現。還可以使用sync.Cond實現單薄或者是廣播時間。

對於close或者是nil通道,傳送和接收操作都有響應的規則:

1.向已經關閉通道傳送資料,引發panic。

2.從已經關閉接收資料,返回已經緩衝資料或者是零值。

3.無論收發,nil通道都會阻塞。

通道預設是雙向的,並不區分傳送和接收端。但是某些時候,我們可以限制收發操作的方向來獲得更加嚴謹的操作邏輯。

可是使用make建立單向通道,但是沒有任何意義。通常使用型別轉換來獲取單向通道並賦予操作雙方。

如果同時處理多個通道,可以使用select語句,它會隨機選擇一個可用的通道進行收發操作。

如果等全部通道訊息處理結束,可以將已經完成通道設定為nil,這樣他就會被阻塞,不會被select選中。

即使是同一個通道也會隨機選擇case執行。

當所有的通道都不可用時,select會執行default語句,如此可以避免seclect阻塞,但是必須注意處理外層迴圈,以避免陷入空耗。也可以用default處理一些預設的邏輯。

工廠方法將goroutine和通道繫結。鑑於通道本身就是一個併發安全的佇列,可用作ID generator。Pool等用途。

可以使用通道實現訊號量。

標準庫time提供timeout和tick channel實現。

效能:將發往通道的資料打包,減少傳輸次數,可以有效提升效能。從實現上來說,通道佇列依舊使用鎖同步機制,單次獲取更多資料(批處理),可以改善因為頻繁加鎖造成的效能問題。

雖然單詞消耗更多的記憶體,但是效能提升非常明顯。如果陣列改成切片會造成更多記憶體分配次數。

通道可能會引發goroutine leak,確切的說是指goroutine處於傳送狀態或者是接受阻塞狀態,但是一直未被喚醒。垃圾回收器並不收集此類資源,導致他們會在等待佇列里長久休眠形成資源洩露。

通道並不是用來取代鎖的,它們有各自不同的用途,通道傾向於解決邏輯層次的併發處理架構,而鎖則是用來保護區域性範圍內的資料安全。

標準庫sync提供互斥和讀寫鎖以及原子操作。

將Mutex作為匿名欄位時,相關方法必須實現為pointer-receiver,否則會因為複製導致死鎖機制失效。

應將Mutex鎖粒度控制在最小範圍內,及早釋放。

Mutex不支援遞迴,即便是同一goroutine下也會導致死鎖。

建議:

1.對效能要求較高的時候應該避免使用deferUnlock。

2.讀寫併發時,用RWMutex效能會更好一些。

3.對於單個資料防寫,可以嘗試使用原子操作。

4.執行嚴格測試,儘可能開啟資料競爭檢查。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/4798/viewspace-2817255/,如需轉載,請註明出處,否則將追究法律責任。

相關文章