天天看點

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.執行嚴格測試,盡可能打開資料競争檢查。

 本文轉自 棋帥小七 51CTO部落格,原文連結:http://blog.51cto.com/xvjunjie/2055128

繼續閱讀