一.協程(Goroutine)
并發:指程序能夠同時執行多個任務的能力,多線程程序在一個核的cpu上運行,就是并發。
并行:多線程程序在多個核的cpu上運行,就是并行。
并發主要由切換時間片來實現"同時"運行,并行則是直接利用多核實現多線程的運行,go可以設置使用核數,以發揮多核計算機的能力。
協程:
?Go 中的并發執行單位,類似于輕量級的線程。
Goroutine 的調度由 Go 運行時管理,用戶無需手動分配線程。
使用 go 關鍵字啟動 Goroutine。
Goroutine 是非阻塞的,可以高效地運行成千上萬個 Goroutine。
在main()函數調用時就開啟了一個主協程,主協程停止則其他的由主協程創建的分協程也停止
OS線程(操作系統線程)具有棧內存(通常為2MB),一個goroutine的棧在其生命周期開始時只有很小的棧(典型情況下2KB),goroutine的棧不是固定的,他可以按需增大和縮小,goroutine的棧大小限制可以達到1GB
協程的好處:
- 輕量級:協程的創建和切換開銷非常小,可以在一個程序中創建大量協程。
- 自動調度:協程由 Go 運行時自動調度,開發者不需要手動管理線程的創建和銷毀。
- 共享內存:協程之間可以共享相同的地址空間,簡化了內存管理和數據共享。
- 非阻塞:協程之間的通信和同步是非阻塞的,避免了傳統線程中的鎖競爭問題。
協程與線程的區別:
協程:
- 非操作系統提供而是由用戶自行創建和控制的用戶態‘線程’,比線程更輕量級。
- 在一個Go程序中同時創建成百上千個goroutine是非常普遍的,一個goroutine會以一個很小的棧開始其生命周期,一般只需要2KB
線程:
- 由操作系統管理,創建和切換開銷較大,適用于需要高性能和復雜調度的場景。
- 線程是進程的一個執行實體,是CPU調度和分派的基本單位,它是比進程更小的能獨立運行的基本單位。
問題:
通常主協程運行的速度大于創建的分協程,導致程序運行完了,分協程還沒運行就結束了
解決方法:
? ? ? ?1.通過time包中sleep函數讓主協程睡眠一段時間,但是睡眠時間是固定的從而分協程已經運行完但是主協程還在睡眠。
? ? ? ?2.通常在main goroutine 中使用sync.WaitGroup來等待 其他goroutine 完成后再退出。
package mainimport ("fmt""sync"
)// 聲明全局等待組變量
var wg sync.WaitGroupfunc hello() {fmt.Println("hello")wg.Done() // 告知當前goroutine完成
}func main() {wg.Add(1) // 登記1個goroutinego hello()fmt.Println("你好")wg.Wait() // 阻塞等待登記的goroutine完成
}
? ?協程的調度(GMP):
G:表示 goroutine,每執行一次
go f()
就創建一個 G,包含要執行的函數和上下文信息。全局隊列(Global Queue):存放等待運行的 G。
P:表示 goroutine 執行所需的資源,最多有 GOMAXPROCS 個。
P 的本地隊列:同全局隊列類似,存放的也是等待運行的G,存的數量有限,不超過256個。新建 G 時,G 優先加入到 P 的本地隊列,如果本地隊列滿了會批量移動部分 G 到全局隊列。
M:線程想運行任務就得獲取 P,從 P 的本地隊列獲取 G,當 P 的本地隊列為空時,M 也會嘗試從全局隊列或其他 P 的本地隊列獲取 G。M 運行 G,G 執行之后,M 會從 P 獲取下一個 G,不斷重復下去。
Goroutine 調度器和操作系統調度器是通過 M 結合起來的,每個 M 都代表了1個內核線程,操作系統調度器負責把內核線程分配到 CPU 的核上執行。
GOMAXPROCS :Go運行時的調度器使用GOMAXPROCS參數來確定需要使用多少個 OS 線程來同時執行 Go 代碼。默認值是機器上的 CPU 核心數。例如在一個 8 核心的機器上,GOMAXPROCS 默認為 8。Go語言中可以通過runtime.GOMAXPROCS函數設置當前程序并發時占用的 CPU邏輯核心數。(Go1.5版本之前,默認使用的是單核心執行。Go1.5 版本之后,默認使用全部的CPU 邏輯核心數。)
調度器步驟
- 創建與入隊:G(協程)創建后,優先入 P 本地隊列,若本地隊列滿,會放到全局隊列。
- 調度準備:P 綁定 M(內核線程映射),M 從 P 本地隊列取 G 執行;本地隊列空時,會從全局隊列或其他 P 偷取 G 補充。
- 執行與切換:M 執行 G,若 G 阻塞(如 I/O 操作),P 會解綁當前 M、關聯新 M 繼續調度其他 G;G 恢復后重新入隊等待執行 。
- 資源協調:GOMAXPROCS 控制 P 數量,協調 CPU 核心(通過操作系統調度器)與 M 映射,讓 G 高效利用計算資源,實現并發調度 。
二.通道(channel)
問題:
單純地將函數并發執行是沒有意義的。函數與函數間需要交換數據才能體現并發執行函數的意義。雖然可以使用共享內存(通過調用函數)進行數據交換,但是共享內存在不同的 goroutine 中容易發生競態問題。為了保證數據交換的正確性,很多并發模型中必須使用互斥量對內存進行加鎖,這種做法勢必造成性能問題。
channel:
單純地將函數并發執行是沒有意義的。函數與函數間需要交換數據才能體現并發執行函數的意義。從而引出通道實現協程之前的數據傳輸與共享
channel特點:
- Go 語言中的通道(channel)是一種特殊的類型。通道像一個傳送帶或者隊列,總是遵循先入先出(First In First Out)的規則,保證收發數據的順序
- 每一個通道都是一個具體類型的導管,也就是聲明channel的時候需要為其指定元素類型。
- 支持同步和數據共享,避免了顯式的鎖機制。
- 使用?
chan
?關鍵字創建,通過?<-
?操作符發送和接收數據。 - 聲明的通道類型變量需要使用內置的make函數初始化之后才能使用
make(chan 元素類型, [緩沖大小])//默認值為nil
var ch chan int
fmt.Println(ch) // <nil>ch4 := make(chan int)
ch5 := make(chan bool, 1) // 聲明一個緩沖區大小為1的通道x := <- ch // 從ch中接收值并賦值給變量x
<-ch // 從ch中接收值,忽略結果<- chan int // 只接收通道,只能接收不能發送
chan <- int // 只發送通道,只能發送不能接收
緩存通道和無緩存通道的區別:
無緩存通道:無緩沖的通道只有在有接收方能夠接收值的時候才能發送成功,否則會一直處于等待發送的階段。同理,如果對一個無緩沖通道執行接收操作時,沒有任何向通道中發送值的操作那么也會導致接收操作阻塞
緩存通道:只要通道的容量大于零,那么該通道就屬于有緩沖的通道,通道的容量表示通道中最大能存放的元素數量。當通道內已有元素數達到最大容量后,再向通道執行發送操作就會阻塞,除非有從通道執行接收操作。
注意事項:
- 對一個關閉的通道再發送值就會導致 panic。
- 對一個關閉的通道進行接收會一直獲取值直到通道為空。
- 對一個關閉的并且沒有值的通道執行接收操作會得到對應類型的零值。
- 關閉一個已經關閉的通道會導致 panic。
三.鎖
當多個 goroutine 同時操作一個資源(臨界區)或者一個全局變量資源時的情況,這種情況下就會發生競態問題(數據競態)如多個協程同時對map進行存和刪除
互斥鎖:
互斥鎖是一種常用的控制共享資源訪問的方法,它能夠保證同一時間只有一個 goroutine 可以訪問共享資源。Go 語言中使用sync包中提供的Mutex類型來實現互斥鎖。
sync.Mutex提供了兩個方法供我們使用。
func (m *Mutex) Lock()?? ?獲取互斥鎖
func (m *Mutex) Unlock()?? ?釋放互斥鎖
讀寫互斥鎖
互斥鎖是完全互斥的,但是實際上有很多場景是讀多寫少的,當我們并發的去讀取一個資源而不涉及資源修改的時候是沒有必要加互斥鎖的,這種場景下使用讀寫鎖是更好的一種選擇。讀寫鎖在 Go 語言中使用sync包中的RWMutex類型。
func (rw *RWMutex) Lock()?? ?獲取寫鎖
func (rw *RWMutex) Unlock()?? ?釋放寫鎖
func (rw *RWMutex) RLock()?? ?獲取讀鎖
func (rw *RWMutex) RUnlock()?? ?釋放讀鎖
func (rw *RWMutex) RLocker() Locker?? ?返回一個實現Locker接口的讀寫鎖
讀寫鎖分為兩種:讀鎖和寫鎖。當一個 goroutine 獲取到讀鎖之后,其他的 goroutine 如果是獲取讀鎖會繼續獲得鎖,如果是獲取寫鎖就會等待;而當一個 goroutine 獲取寫鎖之后,其他的 goroutine 無論是獲取讀鎖還是寫鎖都會等待。
四.協程控制
1.通過多返回值,判斷通道是否已經關閉
value, ok := <- ch
value:從通道中取出的值,如果通道被關閉則返回對應類型的零值。
ok:通道ch關閉時返回 false,否則返回 true。
2.for-range來循環遍歷接收通道的值,當通道的值為空時,結束循環
func f3(ch chan int) {
for v := range ch {
fmt.Println(v)
}
}
通常我們會選擇使用for range循環從通道中接收值,當通道被關閉后,會在通道內的所有值被接收完畢后會自動退出循環。
注意:ch通道在遍歷到最后的數據的時候,如果通道沒有關閉,導致 range 一直等待新的數據,而發送方已經發送完所有數據。所以發送方發送完數據必須關閉才能用range。
3.select多路復用
在某些場景下我們可能需要同時從多個通道接收數據。通道在接收數據時,如果沒有數據可以被接收那么當前 goroutine 將會發生阻塞。你也許會寫出如下代碼嘗試使用遍歷的方式來實現從多個通道中接收值。
for{
// 嘗試從ch1接收值
data, ok := <-ch1
// 嘗試從ch2接收值
data, ok := <-ch2
…
}
這種方式雖然可以實現從多個通道接收值的需求,但是程序的運行性能會差很多。Go 語言內置了select關鍵字,使用它可以同時響應多個通道的操作。
Select 的使用方式類似于之前學到的 switch 語句,它也有一系列 case 分支和一個默認的分支。每個 case 分支會對應一個通道的通信(接收或發送)過程。select 會一直等待,直到其中的某個 case 的通信操作完成時,就會執行該 case 分支對應的語句。具體格式如下:
select {
case <-ch1:
//...
case data := <-ch2:
//...
case ch3 <- 10:
//...
default:
//默認操作
}
Select 語句具有以下特點:
- 可處理一個或多個 channel 的發送/接收操作。
- 如果多個 case 同時滿足,select 會隨機選擇一個執行。
- 對于沒有 case 的 select 會一直阻塞,可用于阻塞 main 函數,防止退出。
- 如果一個case中有多個通道發送數據,則接收時會串行排列等待被接受
4.通過一個新的通道數據來控制主協程等待分協程執行完在執行
cah1 := make(chan int, 100)cha2 := make(chan bool)go s(cah1, cha2)for k := 0; k < 100; k++ {cah1 <- k*2 - 1}close(cah1)<-cha2
}// 控制主協程等待分協程運行,可以用通道信號,當分協程中要執行的內容執行完
// 在向cha2發送信號,在主協程等待信號,這樣的話就可以讓主協程等待
func s(cah1 chan int, cha2 chan bool) {for v := range cah1 {fmt.Println(v)}cha2 <- false
}