Go底層channel實(shí)現(xiàn)原理及示例詳解
概念:
Go中的channel 是一個隊列,遵循先進(jìn)先出的原則,負(fù)責(zé)協(xié)程之間的通信(Go 語言提倡不要通過共享內(nèi)存來通信,而要通過通信來實(shí)現(xiàn)內(nèi)存共享,CSP(Communicating Sequential Process)并發(fā)模型,就是通過 goroutine 和 channel 來實(shí)現(xiàn)的)
使用場景:
停止信號監(jiān)聽
定時任務(wù)
生產(chǎn)方和消費(fèi)方解耦
控制并發(fā)數(shù)
底層數(shù)據(jù)結(jié)構(gòu):
通過var聲明或者make函數(shù)創(chuàng)建的channel變量是一個存儲在函數(shù)棧幀上的指針,占用8個字節(jié),指向堆上的hchan結(jié)構(gòu)體
源碼包中src/runtime/chan.go
定義了hchan的數(shù)據(jù)結(jié)構(gòu):
hchan結(jié)構(gòu)體:
type hchan struct { closed uint32 // channel是否關(guān)閉的標(biāo)志 elemtype *_type // channel中的元素類型 // channel分為無緩沖和有緩沖兩種。 // 對于有緩沖的channel存儲數(shù)據(jù),使用了 ring buffer(環(huán)形緩沖區(qū)) 來緩存寫入的數(shù)據(jù),本質(zhì)是循環(huán)數(shù)組 // 為啥是循環(huán)數(shù)組?普通數(shù)組不行嗎,普通數(shù)組容量固定更適合指定的空間,彈出元素時,普通數(shù)組需要全部都前移 // 當(dāng)下標(biāo)超過數(shù)組容量后會回到第一個位置,所以需要有兩個字段記錄當(dāng)前讀和寫的下標(biāo)位置 buf unsafe.Pointer // 指向底層循環(huán)數(shù)組的指針(環(huán)形緩沖區(qū)) qcount uint // 循環(huán)數(shù)組中的元素數(shù)量 dataqsiz uint // 循環(huán)數(shù)組的長度 elemsize uint16 // 元素的大小 sendx uint // 下一次寫下標(biāo)的位置 recvx uint // 下一次讀下標(biāo)的位置 // 嘗試讀取channel或向channel寫入數(shù)據(jù)而被阻塞的goroutine recvq waitq // 讀等待隊列 sendq waitq // 寫等待隊列 lock mutex //互斥鎖,保證讀寫channel時不存在并發(fā)競爭問題 }
等待隊列:
雙向鏈表,包含一個頭結(jié)點(diǎn)和一個尾結(jié)點(diǎn)
每個節(jié)點(diǎn)是一個sudog結(jié)構(gòu)體變量,記錄哪個協(xié)程在等待,等待的是哪個channel,等待發(fā)送/接收的數(shù)據(jù)在哪里
type waitq struct { first *sudog last *sudog } type sudog struct { g *g next *sudog prev *sudog elem unsafe.Pointer c *hchan ... }
操作:
創(chuàng)建
使用 make(chan T, cap)
來創(chuàng)建 channel,make 語法會在編譯時,轉(zhuǎn)換為 makechan64
和 makechan
func makechan64(t *chantype, size int64) *hchan { if int64(int(size)) != size { panic(plainError("makechan: size out of range")) } return makechan(t, int(size)) }
創(chuàng)建channel 有兩種,一種是帶緩沖的channel,一種是不帶緩沖的channel
// 帶緩沖 ch := make(chan int, 3) // 不帶緩沖 ch := make(chan int)
創(chuàng)建時會做一些檢查:
- 元素大小不能超過 64K
- 元素的對齊大小不能超過 maxAlign 也就是 8 字節(jié)
- 計算出來的內(nèi)存是否超過限制
創(chuàng)建時的策略:
- 如果是無緩沖的 channel,會直接給 hchan 分配內(nèi)存
- 如果是有緩沖的 channel,并且元素不包含指針,那么會為 hchan 和底層數(shù)組分配一段連續(xù)的地址
- 如果是有緩沖的 channel,并且元素包含指針,那么會為 hchan 和底層數(shù)組分別分配地址
發(fā)送
發(fā)送操作,編譯時轉(zhuǎn)換為runtime.chansend
函數(shù)
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool
阻塞式:
調(diào)用chansend函數(shù),并且block=true
ch <- 10
非阻塞式:
調(diào)用chansend函數(shù),并且block=false
select { case ch <- 10: ... default }
向 channel 中發(fā)送數(shù)據(jù)時大概分為兩大塊:檢查和數(shù)據(jù)發(fā)送,數(shù)據(jù)發(fā)送流程如下:
如果 channel 的讀等待隊列存在接收者goroutine
- 將數(shù)據(jù)直接發(fā)送給第一個等待的 goroutine, 喚醒接收的 goroutine
如果 channel 的讀等待隊列不存在接收者goroutine
- 如果循環(huán)數(shù)組buf未滿,那么將會把數(shù)據(jù)發(fā)送到循環(huán)數(shù)組buf的隊尾
- 如果循環(huán)數(shù)組buf已滿,這個時候就會走阻塞發(fā)送的流程,將當(dāng)前 goroutine 加入寫等待隊列,并掛起等待喚醒
接收
發(fā)送操作,編譯時轉(zhuǎn)換為runtime.chanrecv
函數(shù)
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool)
阻塞式:
調(diào)用chanrecv函數(shù),并且block=true
<ch v := <ch v, ok := <ch // 當(dāng)channel關(guān)閉時,for循環(huán)會自動退出,無需主動監(jiān)測channel是否關(guān)閉,可以防止讀取已經(jīng)關(guān)閉的channel,造成讀到數(shù)據(jù)為通道所存儲的數(shù)據(jù)類型的零值 for i := range ch { fmt.Println(i) }
非阻塞式:
調(diào)用chanrecv函數(shù),并且block=false
select { case <-ch: ... default }
向 channel 中接收數(shù)據(jù)時大概分為兩大塊,檢查和數(shù)據(jù)發(fā)送,而數(shù)據(jù)接收流程如下:
如果 channel 的寫等待隊列存在發(fā)送者goroutine
- 如果是無緩沖 channel,直接從第一個發(fā)送者goroutine那里把數(shù)據(jù)拷貝給接收變量,喚醒發(fā)送的 goroutine
- 如果是有緩沖 channel(已滿),將循環(huán)數(shù)組buf的隊首元素拷貝給接收變量,將第一個發(fā)送者goroutine的數(shù)據(jù)拷貝到 buf循環(huán)數(shù)組隊尾,喚醒發(fā)送的 goroutine
如果 channel 的寫等待隊列不存在發(fā)送者goroutine
- 如果循環(huán)數(shù)組buf非空,將循環(huán)數(shù)組buf的隊首元素拷貝給接收變量
- 如果循環(huán)數(shù)組buf為空,這個時候就會走阻塞接收的流程,將當(dāng)前 goroutine 加入讀等待隊列,并掛起等待喚醒
關(guān)閉
關(guān)閉操作,調(diào)用close函數(shù),編譯時轉(zhuǎn)換為runtime.closechan
函數(shù)
close(ch)
func closechan(c *hchan)
案例分析:
package main import ( "fmt" "time" "unsafe" ) func main() { // ch是長度為4的帶緩沖的channel // 初始hchan結(jié)構(gòu)體重的buf為空,sendx和recvx均為0 ch := make(chan string, 4) fmt.Println(ch, unsafe.Sizeof(ch)) go sendTask(ch) go receiveTask(ch) time.Sleep(1 * time.Second) } // G1是發(fā)送者 // 當(dāng)G1向ch里發(fā)送數(shù)據(jù)時,首先會對buf加鎖,然后將task存儲的數(shù)據(jù)copy到buf中,然后sendx++,然后釋放對buf的鎖 func sendTask(ch chan string) { taskList := []string{"this", "is", "a", "demo"} for _, task := range taskList { ch <- task //發(fā)送任務(wù)到channel } } // G2是接收者 // 當(dāng)G2消費(fèi)ch的時候,會首先對buf加鎖,然后將buf中的數(shù)據(jù)copy到task變量對應(yīng)的內(nèi)存里,然后recvx++,并釋放鎖 func receiveTask(ch chan string) { for { task := <-ch //接收任務(wù) fmt.Println("received", task) //處理任務(wù) } }
總結(jié)hchan結(jié)構(gòu)體的主要組成部分有四個:
- 用來保存goroutine之間傳遞數(shù)據(jù)的循環(huán)數(shù)組:buf
- 用來記錄此循環(huán)數(shù)組當(dāng)前發(fā)送或接收數(shù)據(jù)的下標(biāo)值:sendx和recvx
- 用于保存向該chan發(fā)送和從該chan接收數(shù)據(jù)被阻塞的goroutine隊列: sendq 和 recvq
- 保證channel寫入和讀取數(shù)據(jù)時線程安全的鎖:lock
以上就是Go底層channel實(shí)現(xiàn)原理及示例詳解的詳細(xì)內(nèi)容,更多關(guān)于Go channel底層原理的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
golang使用net/rpc庫實(shí)現(xiàn)rpc
這篇文章主要為大家詳細(xì)介紹了golang如何使用net/rpc庫實(shí)現(xiàn)rpc,文章的示例代碼講解詳細(xì),具有一定的借鑒價值,需要的小伙伴可以參考一下2024-01-01golang連接mysql數(shù)據(jù)庫操作使用示例
這篇文章主要為大家介紹了golang連接mysql數(shù)據(jù)庫操作使用示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步早日升職加薪2022-04-04Golang跳轉(zhuǎn)語句continue與goto使用語法詳解
這篇文章主要介紹了Golang跳轉(zhuǎn)語句continue與goto使用語法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)吧2023-01-01一站式解決方案:在Windows和Linux上快速搭建Go語言開發(fā)環(huán)境
本文將介紹如何在Windows和Linux操作系統(tǒng)下搭建Go語言開發(fā)環(huán)境,以幫助您更高效地進(jìn)行Go語言開發(fā),需要的朋友可以參考下2023-10-10Go map底層實(shí)現(xiàn)與擴(kuò)容規(guī)則和特性分類詳細(xì)講解
這篇文章主要介紹了Go map底層實(shí)現(xiàn)與擴(kuò)容規(guī)則和特性,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)吧2023-03-03golang中strconv.ParseInt函數(shù)用法示例
這篇文章主要介紹了golang中strconv.ParseInt函數(shù)用法,實(shí)例分析了strconv.ParseInt函數(shù)將字符串轉(zhuǎn)換為數(shù)字的簡單使用方法,需要的朋友可以參考下2016-07-07Go語言異常處理(Panic和recovering)用法詳解
異常處理是程序健壯性的關(guān)鍵,往往開發(fā)人員的開發(fā)經(jīng)驗的多少從異常部分處理上就能得到體現(xiàn)。Go語言中沒有Try?Catch?Exception機(jī)制,但是提供了panic-and-recover機(jī)制,本文就來詳細(xì)講講他們的用法2022-07-07