并發與并行??
并發與并行的概念和區別
并行:同一個時間段內多個任務同時在不同的CPU核心上執行,強調同一時刻多個任務之間的”同時執行“,
并發:同一個時間段內多個任務都在進展,強調多個任務間的”交替執行“,

隨著硬體水平的提高,現在的終端主機都是多個CPU,每個CPU都是多核結構,當多個CPU同時運行起來,跑不同的任務,這屬于并行;在一個CPU里的多個核心里同時運行不同的任務,同樣也屬于并行,而并發是關注一個核心里的多個任務,這時需要交替執行,就是并發,
CPU是計算單元,有資料才能進行計算,當一個任務被網路I/O阻塞,CPU沒有資料,就會處于等待,顯然,若是能夠將等待的時間利用起來,資源利用率會提高,因此,并發處理的主要目的是提高CPU和資源的利用率,
Go語言并發與CPU核心的關系
- Go并發是基于Goroutine和Channel實作的,
Goroutine是Go語言的并發執行單元,Channel用于Goroutine之間的通信與同步,
- Goroutine的執行依賴于作業系統的執行緒調度,
Goroutine自身不具備執行背景關系,它必須依存在作業系統執行緒上才可以真正執行,當一個Goroutine被創建時,Go runtime會自動選擇一個空閑的作業系統執行緒,將這個Goroutine的執行背景關系系結到該執行緒上,

上圖中G表示Goroutine,P表示一個調度的背景關系(包含了運行 Goroutine 的資源),M表示一個OS執行緒,一個作業系統執行緒可以同時關聯多個Goroutine,這些Goroutine會被Go runtime高效地在該執行緒上調度執行,但任意時刻只會有一個Goroutine獲得執行緒的執行權進行運行(G0就是獲得執行緒權的Goroutine),當關聯的作業系統執行緒終止時,系結在該執行緒上的所有Goroutine也會被終止,
- 在單核CPU上,即使有許多Goroutine,同一時刻也只能有一個Goroutine真正在CPU上運行,
單核CPU同一時刻只能執行一個執行緒,即使有許多Goroutine,也只有獲得CPU執行權的那個Goroutine在真正運行,其他Goroutine會被掛起,等待下次被調度執行,
- 在多核CPU上,作業系統可以將不同的Goroutine直接調度到不同的CPU核心上運行,
這樣多個Goroutine就可以同時真正運行,實作并行執行,此時Go并發程式可以發揮多核CPU的強大計算能力,Go runtime會有智能的調度策略,將Goroutine均勻地分布在所有CPU核心上或者以負載均衡的方式進行調度,這取決于Goroutine的數量和系統的CPU核心數,

GMP模型如上圖所示,圖中涉及5個重要的物體,
- 全域佇列(run queue):存放等待運行的Goroutine,
- 本地佇列(local queue):和P連接的佇列,存放的也是等待運行的Goroutine,存放數量有限,一般不超過256個,新建Goroutine時,優先在本地佇列存放,然后再放置全域佇列,
- P:被稱為處理器,包含了運行 Goroutine 的資源,一個Goroutine想要運行,必須先獲取P,P的數量是可配置的,最多有
GOMAXPROCS個, - G:指代Goroutine,會被Go runtime智能化調度,當一個執行緒M空閑時,首先會從全域佇列里獲取Goroutine,若全域佇列為空,則會從周邊的執行緒”偷“一半Goroutine放到本地佇列,
- M:執行緒由OS調度器分配到CPU的核上執行,當一個執行緒阻塞時,會導致和該執行緒的其它Goroutine”餓死“,此時,Go runtime會解綁P和M,將一個新的M分配給P避免”餓死“情況發生,
Goroutine與OS執行緒的區別
- 生命周期不同
Goroutine由Go runtime 管理生命周期,創建和銷毀由runtime調度完成,OS執行緒是由作業系統內核來管理生命周期,
- 調度不同
Goroutine由Go runtime的調度器進行調度,OS執行緒的調度是由作業系統內核根據時間片進行的,
- 關聯關系不同
每個OS執行緒與一個Goroutine關聯,但一個Goroutine不一定對應一個OS執行緒,多個Goroutine可能對應同一個OS執行緒,Go runtime會動態地將Goroutine映射到執行緒上,
- 資源消耗不同
創建和維護OS執行緒需要較多資源,而Goroutine的資源消耗很小,一個應用程式可以同時存在成千上萬個Goroutine,但OS執行緒數目通常較小,
- 通信方式不同
Goroutine之間通信使用Channel,而OS執行緒通常使用共享記憶體來通信,
- 并發數不同
一個Go程式的并發度可以達到上百萬,這是由于Goroutine的高效率實作,OS執行緒難以達到如此高的并發度,
Goroutine??
Goroutine的概念與特點
概念:Goroutine是一個輕量級的執行單元,用于執行并發任務,多個Goroutine可以在同一地址空間中執行,且Go runtime會管理其生命周期,Goroutine通過Channel進行通信,
特點:
- 輕量級:創建和維護Goroutine的開銷很小,一個程式可以同時存在成千上萬個Goroutine,
- 并發執行:Goroutine允許程式利用多核CPU的優勢進行并發執行,
- 自動調度:Goroutine的調度完全由Go運行時進行管理,開發者不需要關心底層細節,
- 資源共享:多個Goroutine可以訪問共享的記憶體資源,這使得Goroutine之間可以高效地通信與協作,
- 無需回收:Goroutine不需要手動回收即可釋放資源,運行時會自動回收結束的Goroutine,
- 動態擴展:一個Go程式可以從幾個Goroutine開始,然后動態地創建更多Goroutine來利用多核資源,
- 高并發:利用Goroutine可以輕易地撰寫高并發程式,一個服務器程式可以同時接待成千上萬個客戶端,
- Channel通信:Goroutine之間可以通過Channel進行高效的訊息通信與同步,這使得撰寫并發程式變得簡單,
創建Goroutine的語法
go func(){}():第一個func() {...} 定義了一個匿名函式(anonymous function),第二個()代表呼叫這個匿名函式,
package main
import (
"fmt"
"time"
)
func main() {
// 創建一個Goroutine
go func() {
fmt.Println("Hello from Goroutine!")
}()
time.Sleep(1)
// 主Goroutine
fmt.Println("Hello from main!")
}
Goroutine的調度與背景關系切換
為了讓不同的Goroutine有機會運行,runtime會在Goroutine之間進行背景關系切換,當一個Goroutine運行一定時間或遇到channel操作時,會主動交出執行緒的執行權,這時runtime會從其他掛起的Goroutine中選擇一個繼續運行,背景關系切換涉及到保存當前運行Goroutine的程式計數器、堆疊指標等背景關系資訊,并恢復下一個要運行的Goroutine的背景關系資訊,這個程序需要一定的時間開銷,
Goroutine存在的記憶體問題及解決方案
存在的問題:
-
堆疊溢位:每個Goroutine都有一個私有的堆疊,默認堆疊大小為2MB,如果函式呼叫太深會導致堆疊溢位,
-
堆溢位:如果Goroutine中分配過多的堆物件,也會導致記憶體溢位,
-
記憶體泄露:如果Goroutine退出時沒有釋放之前分配的記憶體,會導致這部分記憶體泄漏,
解決方案:
- Goroutine堆疊大小
方法一:設定runtime.GOMAXPROCS(n, stackSize),其中,n指定要使用的P的個數,設定為0表示使用所有CPU核心,stackSize指定新的默認Goroutine堆疊大小,單位為位元組,
方法二:創建Goroutine時,指定堆疊的大小,go func(params) { /* ... */ }(stackSize, params),stackSize必須是第一個引數,在params之前指定,注意:stackSize的引數只在編譯時起作用,用于指定Goroutine的堆疊大小,在被呼叫的函式內部,它無法訪問這個引數,
- 避免無限遞回
在Goroutine中呼叫無限遞回函式會引起堆疊溢位,應該避免這種情況發生,
- 設定垃圾回收閾值
可以通過runtime.GOGC來設定垃圾回收器的閾值,觸發更頻繁的垃圾回收來避免堆溢位,例如runtime.GOGC=200 設定垃圾回收閾值為200,
- 手動回收堆記憶體
當一個Goroutine結束時,其私有堆疊記憶體會被識訓,但堆記憶體不會自動回收,因此,需要在Goroutine結束前手動回收不再使用的堆記憶體,否在會發生記憶體泄露,
go func() {
// 分配一些堆空間
buf := make([]byte, 100)
// 使用buf...
// Goroutine結束前手動回收buf
buf = nil
}()
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done() // Goroutine結束 decrement WaitGroup計數
// 分配記憶體并使用...
buf := make([]byte, 100)
// 使用buf...
buf = nil // 手動回收記憶體
}()
wg.Wait() // 等待Goroutine結束
Channel??
Channel的概念與特點
Channels are not closed by default. They need to be closed explicitly with the
Closemethod to indicate that no more values will be sent on the channel.
Channel是一個通信機制,它可以使多個Goroutine之間相互發送資料,Channel允許任意兩個Goroutine通過它異步地傳遞資訊,

特點:
-
方向性:Channel可以是雙向的(default)或單向的(指定方向時),單向Channel按發送/接收方向分為發送(chan<-)和接收(<-chan)Channel,
-
型別安全:Channel在宣告時需指定元素型別,之后只能傳送該型別的元素,這保證了Channel通信的型別安全,
-
FIFO:Channel實作了先入先出的規則,發送的元素按順序被接收,
-
阻塞:向一個滿的Channel發送資料會導致發送方阻塞,從一個空的Channel接收資料會導致接收方阻塞,
-
緩沖:可以指定Channel的緩沖區大小,向一個未滿的緩沖Channel發送資料不會阻塞,
-
關閉:關閉的Channel無法再發送資料,但可以繼續從中接收資料,向關閉的Channel發送資料會panic,
-
無緩沖或滿的Channel導致Goroutine阻塞,這可用于實作同步和協作,
-
Channel支持for range形式的接收,這會不斷接收Channel的資料知道它被關閉,
-
Channel可用于函式間傳遞資料,實作異步執行的函式之間的資料通信,
無緩沖Channel與有緩沖Channel
- 無緩沖Channel
unbuffered channel 就是緩沖大小為 0 的 channel,無緩沖區的 channel 本身是不存放資料的,在發送和接收都會被阻塞,也就是相當于,你現在是一個 send 身份,但是當另外一個沒有 receive 你發送的值之前,你一直處于阻塞(等待接收)狀態;就好比你遞東西給別人,別人沒接,你就要一直舉著東西,相反,如果你現在是一個 receive 身份,你就會一直阻塞(等待發送)狀態,在你拿到值之前,你會一直等待,就好比你準備要接東西,別人遲遲不給你,你就要一直等著,
package main
import (
"fmt"
)
func main() {
ch := make(chan struct{})
go func() {
defer close(ch)
v := <-ch
fmt.Printf("receive a struct: %v\n", v)
}()
ch <- struct{}{}
fmt.Println("send a struct")
}
- 有緩沖Channel

有緩沖的Channel就是設定了一個buffersize,作為緩沖區的大小,
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int, 1) // 設定buffersize大小為1
go func() {
defer close(ch)
defer fmt.Println("我關閉了")
ch <- 1
ch <- 2
fmt.Println("send a struct")
}()
time.Sleep(5 * time.Second)
for {
v, ok := <-ch
if !ok {
fmt.Println("channel closed")
break
}
fmt.Printf("received a struct %v\n", v)
}
}
// output
received a struct 1
received a struct 2
send a struct
我關閉了
channel closed
創建Channel時,當buffersize==1時,相當于buffersize==0,表示創建一個無緩沖的通道,所以,當向通道傳入一個1時,緩沖區就滿了,此時,必須等接收端將1讀走,才能繼續向channel發送2,
將buffersize修改為2時,表示創建一個有緩沖的通道,發送者會一次性將1和2發送到通道里,然后就關閉Channel的寫入端,睡眠5秒后,就由接收端將結果讀出,
// output
send a struct
我關閉了
received a struct 1
received a struct 2
channel closed
應用:實作一個生產者消費者模型
package main
import (
"fmt"
"time"
)
func producer(out chan<- int) {
for i := 0; i < 10; i++ {
out <- i // 生產一個資料,發送到Channel
fmt.Println("生產者:", i)
}
close(out)
}
func consumer(in <-chan int) {
for num := range in { // 從Channel中接收資料
fmt.Println("消費者:", num) // 消費資料
}
}
func main() {
// 創建一個管道
ch := make(chan int)
go producer(ch) // 啟動生產者Goroutine
go consumer(ch) // 啟動消費者Goroutine
go consumer(ch) // 啟動消費者Goroutine
time.Sleep(5 * time.Second) // 等待
}
既然都看到這里了,不如順手點個推薦吧!

參考資料:
https://morsmachine.dk/go-scheduler
https://go.dev/blog/pipelines
https://www.kelche.co/blog/go/golang-scheduling/
https://www.kelche.co/blog/go/channels/
https://juejin.cn/post/7231887884739346489
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/553947.html
標籤:其他
上一篇:StampedLock:高并發場景下一種比讀寫鎖更快的鎖
下一篇:返回列表
