1. 程式人生 > >Go語言並發編程簡單入門

Go語言並發編程簡單入門

go語言 並發編程 入門

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

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

多線程或者多進程是並行的基本條件,但是單線程也可以用協程做到並發。盡管協程在單線程上通過主動切換來實現多任務並發,但它也有自己的優勢。協程上運行的多個任務本質上是串行執行的,加上可控自主調度,所以並不需要做同步處理。

即使采用多線程也未必就能執行並行。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.執行嚴格測試,盡可能打開數據競爭檢查。


Go語言並發編程簡單入門