go語言併發程式設計
引言
說到go語言最厲害的是什麼就不得不提到併發,併發是什麼?,與併發相關的並行又是什麼?
併發:同一時間段內執行多個任務
並行:同一時刻執行多個任務
程序、執行緒與協程
- 程序:
程序是具有一定獨立功能的程式關於某個資料集合上的一次執行活動,程序是系統進行資源分配和排程的一個獨立單位。每個程序都有自己的獨立記憶體空間,不同程序通過程序間通訊來通訊。由於程序比較重量,佔據獨立的記憶體,所以上下文程序間的切換開銷(棧、暫存器、虛擬記憶體、檔案控制代碼等)比較大,但相對比較穩定安全。 - 執行緒:
執行緒是程序的一個實體,是CPU排程和分派的基本單位,它是比程序更小的能獨立執行的基本單位.執行緒自己基本上不擁有系統資源,只擁有一點在執行中必不可少的資源(如程式計數器,一組暫存器和棧),但是它可與同屬一個程序的其他的執行緒共享程序所擁有的全部資源。執行緒間通訊主要通過共享記憶體,上下文切換很快,資源開銷較少,但相比程序不夠穩定容易丟失資料。 - 協程:
協程是一種使用者態的輕量級執行緒,協程的排程完全由使用者控制。協程擁有自己的暫存器上下文和棧。協程排程切換時,將暫存器上下文和棧儲存到其他地方,在切回來的時候,恢復先前儲存的暫存器上下文和棧,直接操作棧則基本沒有核心切換的開銷,可以不加鎖的訪問全域性變數,所以上下文的切換非常快。
goroutine
go語言原生支援併發,可以用go關鍵字快速的讓一個函式建立為goroutine協程,也可以建立多個goroutine去執行相同的函式。
sync.WaitGroup可以用來實現goroutine的同步
例如:
var wg sync.WaitGroup func hello(i int) { defer wg.Done() // goroutine結束就-1 fmt.Println("Hello Goroutine!", i) } func main() { for i := 0; i < 10; i++ { wg.Add(1) // 啟動一個goroutine就+1 go hello(i) } wg.Wait() // 等待所有登記的goroutine都結束 }
最終打印出來的順序是亂序,因為goroutine是併發操作。
goroutine實際上就是go中的協程,在go語言中可以起成千上萬個goroutine協程來進行併發程式設計
goroutine的排程
goroutine的排程基於GMP模型
- G代表一個goroutine物件,每次go呼叫的時候,都會建立一個G物件
- M代表一個執行緒,每次建立一個M的時候,都會有一個底層執行緒建立;所有的G任務,最終還是在M上執行
- P代表一個處理器,每一個執行的M都必須繫結一個P,就像執行緒必須在麼一個CPU核上執行一樣
P的個數就是GOMAXPROCS(最大256),啟動時固定的,一般不修改; M的個數和P的個數不一定一樣多(會有休眠的M或者不需要太多的M)(最大10000);每一個P儲存著本地G任務佇列,也有一個全域性G任務佇列;
併發安全
go原生提供併發原語goroutine和channel為構造併發提供了一種優雅而簡單的方式,go沒有顯示的利用鎖來控制併發安全,而是鼓勵提倡通過通訊共享記憶體而不是通過共享記憶體而實現通訊。
sync.atomic
Go語言中原子操作由內建的標準庫sync/atomic提供。
這些功能需要非常小心才能正確使用。 除特殊的底層應用程式外,同步更適合使用channel或sync包的功能。 通過訊息共享記憶體; 不要通過共享記憶體進行通訊。
Mutex
互斥鎖是一種常用的共享資源訪問的方法,它能夠保證同時只有一個goroutine可以訪問資源。Go語言中使用sync包的Mutex型別來實現互斥鎖。
go在1.8預設使用自旋模式,當試圖獲取已經被持有的鎖時,如果本地佇列為空並且 P 的數量大於1,goroutine 將自旋幾次(用一個 P 旋轉會阻塞程式)。自旋後,goroutine park。在程式高頻使用鎖的情況下,它充當了一個快速路徑。
go在1.9新增了Starving模式,當自旋模式搶到鎖,表示有協程釋放了鎖,如果waiter>0,即有阻塞等待的協程,會釋放訊號量來喚醒協程,當協程被喚醒後,發現Locked=1,鎖又被搶佔,則又會阻塞,但在阻塞前會判斷自上次阻塞到本次阻塞經歷了多長時間,如果超過1ms的話,會將Mutex標記為"飢餓"模式,然後再阻塞。當被標記為飢餓狀態時,unlock 方法會 handsoff 把鎖直接扔給第一個等待者。
在飢餓模式下,自旋也被停用,因為傳入的goroutines 將沒有機會獲取為下一個等待者保留的鎖。
RWMutex
互斥鎖是完全互斥的,但是有很多場景下讀多寫少,因此我們併發去讀取一個資源而不涉及到資源修改的時候是完全沒必要加鎖的,這種情況下讀寫鎖是一種更好的選擇。
讀寫鎖分為讀鎖和寫鎖,讀鎖與讀鎖相容,讀鎖與寫鎖互斥,寫鎖與寫鎖互斥。
errgroup
ErrGroup是 Go 官方提供的一個同步擴充套件庫。可以將一個大任務拆分成幾個小任務併發執行,提高程式效率。sync.ErrGroup在sync.WaitGroup功能的基礎上,增加了錯誤傳遞,以及在發生不可恢復的錯誤時取消整個goroutine集合,或者等待超時
sync.pool
go語言為了降低GC壓力引入了sync.Pool物件池用來儲存和複用臨時物件。sync.Pool是可伸縮的,併發安全的。其大小僅受限於記憶體的大小。sync.pool物件池比較適合用來儲存一些臨時切狀態無關的資料,但是不適合用來做連線池,因為存入物件池中的值有可能會在垃圾回收時被刪除掉
在go的1.13版本中引入了victim cache,會將pool內資料拷貝一份,避免GC將其清空,即使沒有引用的內容也可以保留最多兩輪GC.
channel
channel是一種型別安全的訊息佇列,用以充當兩個goroutine之間的訊息通道。go語言的併發模型是CSP(Communicating Sequential Processes),提倡通過通訊共享記憶體而不是通過共享記憶體而實現通訊。
go語言中的channel是一種特殊的型別,遵循先入先出的規則,保證資料的收發順序。
無緩衝通道
//建立語法
ch := make(chan int)
無緩衝通道沒有容量,因此無緩衝的通道只有在有接收者的時候才能傳送,否則會形成死鎖,相反如果接收操作先執行,接收方的goroutine將阻塞,直到另一個goroutine在該通道上傳送一個值。
func main() {
ch := make(chan int)
go func() {
fmt.Println(<-ch)
}()
ch <- 10
}
無緩衝管道的本質是為保證同步
有緩衝通道
//建立語法
ch := make(chan int, 10) //建立緩衝為10的通道
只要通道的容量大於零,那麼該通道就是有緩衝的通道,通道的容量表示通道中能存放元素的數量,當通道的容量已滿時將會阻塞傳送者使其等待緩衝通道可用,而當緩衝通道為空的時候會阻塞接收者使其等待資源被髮送。
channel內建的len函式可以獲取通道內元素的數量,使用cap函式獲取通道的容量。
常見異常
References
https://www.cnblogs.com/lxmhhy/p/6041001.html
https://blog.csdn.net/liangzhiyang/article/details/52669851
https://www.cnblogs.com/sunsky303/p/9705727.html
https://zhuanlan.zhihu.com/p/265670936
https://zhuanlan.zhihu.com/p/88878287
https://www.bilibili.com/read/cv10112308/
https://pkg.go.dev/golang.org/x/sync/errgroup
https://mp.weixin.qq.com/s/NcrENqRyK9dYrOBBI0SGkA
https://www.jianshu.com/p/8fbbf6c012b2
https://www.jianshu.com/p/24ede9e90490
https://www.liwenzhou.com/posts/Go/14_concurrence/#autoid-1-4-3