引言
說到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
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/373734.html
標籤:Go
