引言
說到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