golang channel底層結構和實現
- 2022 年 10 月 4 日
- 筆記
一、介紹
Golang 設計模式: 不要通過共享記憶體來通訊,而要通過通訊實現記憶體共享
channel是基於通訊順序模型(communication sequential processes, CSP)的併發模式,可以讓一個 goroutine 發送特定值到另一個 goroutine 的通訊機制
channel中的數據遵循先入先出(First In First Out)的規則,保證收發數據的順序
二、結構
channel的源碼在runtime包下的chan.go文件, 參見chan.go
以下時channel的部分結構:
type hchan struct { qcount uint dataqsiz uint buf unsafe.Pointer elemsize uint16 closed uint32 elemtype *_type sendx uint recvx uint recvq waitq sendq waitq lock mutex } type waitq struct { first *sudog last *sudog }
其中:
qcount: 隊列中剩餘的元素個數 dataqsiz: 環形隊列長度,即可以存放的元素個數, make初始化時指定 buf: 快取區,實際上就是環形隊列(有環形隊列就有緩衝區,否則沒有緩衝區),指向環形隊列首部的指針,基於環形隊列實現,大小等於make初始化channel時指定的環形隊列長度,如果make初始化channel時不指定dataqsiz,則buf=0。只有緩衝型的channel才有buf elemsize: 每個元素的大小 closed: channel關閉標誌 elemtype: 元素類型 sendx: 寫入數據的索引,即從哪個位置開始寫入數據,取值[0, dataqsiz) recvx: 讀取數據的索引,即從哪個位置開始讀取數據,取值[0, dataqsiz) recvq: 接收等待隊列,鏈表結構,長度無限長, 讀取數據的goroutine等待隊列, 如果channel的緩衝區為空或者沒有緩衝區,讀取數據的goroutine被阻塞,加入到recvq等待隊列中。因讀阻塞的goroutine會被向channel寫入數據的goroutine喚醒 sendq: 發送等待隊列,鏈表結構,長度無限長, 寫入數據的goroutine等待隊列, 如果channel的緩衝區為滿或者沒有緩衝區,寫入數據的goroutine被阻塞,加入到sendq等待隊列中。因寫阻塞的goroutine會被從channel讀取數據的goroutine喚醒 lock: 並發控制鎖, 同一時刻,只允許一個, channel不允許並發讀寫
1 結構圖
其中:
環形隊列中的0表示沒有數據,1表示有數據; G表示一個goroutine dataqsiz表示環形隊列的長度為6, 即可快取6個元素 buf指向環形隊列首部,此時還可以快取2個元素 qcount表示環形隊列中有4個元素 sendx表示下一個發送的數據在環形隊列index=5的位置寫入,取值[0, 6) recvx表示從環形隊列index=1的位置讀取數據,取值[0, 6) sendq, recvq: 虛線表示,此時轉態下的channel可能有等待隊列
三、channel的創建
1 聲明channel類型
//同時讀寫的channel var 變數 chan 類型 //只能寫入數據的channel var 變數 chan<- 類型 //只能讀取數據的channel var 變數 <-chan 類型
其中:
類型:channel內的數據類型,golang支援的合法類型
聲明的channel此時還是nil,需要配合make函數初始化之後才能使用
2 創建channel
//無緩衝的channel 變數 := make(chan 數據類型) //有緩衝的channel 變數 := make(chan 數據類型, dataqsiz)
四、向channel發送數據
1 發送數據的格式
變數 <- 值
2 寫數據的過程
1) 流程圖如下:
其中:
G表示一個goroutine 虛線表示sendq中堵塞的G被喚醒的流程,如果G沒有被喚醒,則一直堵塞下去,此時關閉channel,會觸發panic
2) 過程描述:
1) 如果channel是nil(沒有初始化), 發送數據則一直會堵塞,這是一個BUG 2) 如果等待接收隊列recvq 不為空,說明沒有緩衝區或者緩衝區沒有數據,直接從recvq取出一個G數據寫入,把G喚醒,結束髮送過程 3) 如果等待接收隊列recvq為空,且緩衝區有空位,那麼就直接將數據寫入緩衝區sendx位置, sendx++, qcount++, 結束髮送過程 4) 如果等待接收隊列recvq為空,緩衝區沒有空位,將數據寫入G,然後把G放到等待發送隊列sendq中進行阻塞,等待被喚醒, 結束髮送過程。當被喚醒的時候,需要寫入的數據已經被讀取出來,且已經完成了寫入操作
五、從channel接收數據
1 接收數據的格式
1) 阻塞接收數據
程式阻塞直到收到數據並賦值
data := <-ch
2) 非阻塞接收數據
非阻塞的通道接收方法可能造成高的 CPU 佔用
//ok表示是否接收到數據 data, ok := <-ch
3) 接收數據並忽略
程式阻塞直到接收到數據,但接收到的數據會被忽略
<-ch
4) 循環接收
channel是可以進行遍歷的,遍歷的結果就是接收到的數據
for data := range ch { //done }
5) SELECT語句接收
select 的特點是只要其中有一個 case 已經完成,程式就會繼續往下執行,而不會考慮其他 case 的情況
在一個 select 語句中,Go語言會按順序從頭至尾評估每一個發送和接收的語
如果其中的多條case語句可繼續執行(即沒有被阻塞),那麼就從這些case語句中任意選擇一條
如果沒有case語句可以執行(即所有的通道都被阻塞):
1) 如果有 default 語句,執行 default 語句,同時程式的執行會從 select 語句後的語句中恢復
2) 如果沒有 default 語句,那麼 select 語句將被阻塞,直到至少有一個case可以進行下去
select { case <- chan1: //done case chan2 <- 2: //done default: //done }
2 讀取數據的流程
1) 流程圖如下:
其中:
G表示一個goroutine 虛線表示recvq中堵塞的G被喚醒的流程,如果G沒有被喚醒,則一直堵塞下去,此時關閉channel,會得到channel類型的零值
2) 過程描述:
1 如果等待發送隊列sendq不為空,且沒有緩衝區,直接從sendq中取出G,讀取數據,最後把G喚醒,結束讀取過程 2 如果等待發送隊列sendq不為空,有緩衝區(此時緩衝區滿了),從緩衝區中首部讀出數據,把sendq出列的G中數據寫入緩衝區尾部,把G喚醒,結束讀取過程 3 如果等待發送隊列sendq為空,且環形隊列無元素,將goruntime加入等待接收隊列recvq中進行堵塞,等待被喚醒 4 如果等待發送隊列sendq為空,環形隊列有元素,直接從緩衝區讀取數據,結束讀取過程
六、關閉channel
1 格式
close(ch)
2 過程描述
1) 首先校驗chan是否已被初始化,然後加鎖之後再校驗是否已被關閉過,如果校驗都通過了,那麼將closed欄位設值為1 2) 遍歷recvq和sendq,並將所有的goroutine 加入到glist中 3) 將所有glist中的goroutine加入調度隊列,等待被喚醒 4) recvq中的goroutine接收到對應數據的零值,sendq中的goroutine會直接panic
七、channel發送、接收數據過程可能產生的問題
1 向一個nil的channel發送/讀取數據會一直堵塞下去?該如何喚醒?
會一直堵塞下去,不會被喚醒,可能會造成泄露,這是一個BUG
2 等待發送隊列(sendq)中有數據,如果一直沒有gouruntine從channel裡面讀數據會不會造成泄漏?
會造成泄露,channel用完了,最好要close
3 向已經關閉的channel讀/寫數據會發生什麼?
寫已經關閉的 channel 會觸發panic
讀已經關閉的 channel,能一直讀到數據:
1) 如果 channel 關閉前,buf內有元素還未讀,會正確讀到 channel 內的值,且返回的第二個 bool 值為 true
2) 如果 channel 關閉前,buf內有元素已經被讀完,channel 內無值,返回 channel 元素的零值,第二個 bool 值為 false
4 觸發 panic 的三種情況
1) 向一個關閉的 channel 進行寫操作
2) 關閉一個為 nil 的 channel
3) 重複關閉一個 channel