深入理解golang chan的使用
前言
之前在看golang
多線程通信的時候, 看到了go 的管道. 當時就覺得這玩意很神奇, 因為之前接觸過的不管是php
, java
, Python
, js
, c
等等, 都沒有這玩意, 第一次見面, 難免勾起我的好奇心. 所以就想著看一看它具體是什么東西. 很明顯, 管道是go
實現(xiàn)在語言層面的功能, 所以我以為需要去翻他的源碼了. 雖然最終沒有翻到C
的層次, 不過還是受益匪淺.
見真身
結構體
要想知道他是什么東西, 沒什么比直接看他的定義更加直接的了. 但是其定義在哪里么? 去哪里找呢? 還記得我們是如何創(chuàng)建chan
的么? make
方法. 但是當我找過去的時候, 發(fā)現(xiàn)make
方法只是一個函數的聲明.
這, 還是沒有函數的具體實現(xiàn)啊. 匯編看一下. 編寫以下內容:
package main func main() { _ = make(chan int) }
執(zhí)行命令:
go tool compile -N -l -S main.go
雖然匯編咱看不懂, 但是其中有一行還是引起了我的注意.
make
調用了runtime.makechan
. 漂亮, 就找他.
找到他了, 是hchan
指針對象. 整理了一下對象的字段(不過人家自己也有注釋的):
// 其內部維護了一個循環(huán)隊列(數組), 用于管理發(fā)送與接收的緩存數據. type hchan struct { // 隊列中元素個數 qcount uint // 隊列的大小(數組長度) dataqsiz uint // 指向底層的緩存隊列, 是一個可以指向任意類型的指針. buf unsafe.Pointer // 管道每個元素的大小 elemsize uint16 // 是否被關閉了 closed uint32 // 管道的元素類型 elemtype *_type // 當前可以發(fā)送的元素索引(隊尾) sendx uint // 當前可以接收的元素索引(隊首) recvx uint // 當前等待接收數據的 goroutine 隊列 recvq waitq // 當前等待發(fā)送數據的 goroutine 隊列 sendq waitq // 鎖, 用來保證管道的每個操作都是原子性的. lock mutex }
可以看的出來, 管道簡單說就是一個隊列加一把鎖.
發(fā)送數據
依舊使用剛才的方法分析, 發(fā)送數據時調用了runtime.chansend1
函數. 其實現(xiàn)簡單易懂:
然后查看真正實現(xiàn), 函數步驟如下(個人理解, 有一些 test 使用的代碼被我刪掉了. ):
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool { // 異常處理, 若管道指針為空 if c == nil { if !block { return false } gopark(nil, nil, waitReasonChanSendNilChan, traceEvGoStop, 2) throw("unreachable") } // 常量判斷, 恒為 false, 應該是開發(fā)時調試用的. if debugChan { print("chansend: chan=", c, "\n") } // 常量, 恒為 false, 沒看懂這個判斷 if raceenabled { racereadpc(c.raceaddr(), callerpc, funcPC(chansend)) } // 若當前操作不阻塞, 且管道還沒有關閉時判斷 // 當前隊列容量為0且沒有等待接收數據的 或 當前隊列容量不為0且隊列已滿 // 那么問題來了, 什么時候不加鎖呢? select 的時候. 可以在不阻塞的時候快速返回 if !block && c.closed == 0 && ((c.dataqsiz == 0 && c.recvq.first == nil) || (c.dataqsiz > 0 && c.qcount == c.dataqsiz)) { return false } // 上鎖, 保證操作的原子性 lock(&c.lock) // 若管道已經關閉, 報錯 if c.closed != 0 { unlock(&c.lock) panic(plainError("send on closed channel")) } // 從接受者隊列獲取一個接受者, 若存在, 數據直接發(fā)送, 不走緩存, 提高效率 if sg := c.recvq.dequeue(); sg != nil { send(c, sg, ep, func() { unlock(&c.lock) }, 3) return true } // 若緩存為滿, 則將數據放到緩存中排隊 if c.qcount < c.dataqsiz { // 取出對尾的地址 qp := chanbuf(c, c.sendx) // 將ep 的內容拷貝到 ap 地址 typedmemmove(c.elemtype, qp, ep) // 更新隊尾索引 c.sendx++ if c.sendx == c.dataqsiz { c.sendx = 0 } c.qcount++ unlock(&c.lock) return true } // 若當前不阻塞, 直接返回 if !block { unlock(&c.lock) return false } // 當走到這里, 說明數據沒有成功發(fā)送, 且需要阻塞等待. // 以下代碼沒看懂, 不過可以肯定的是, 其操作為阻塞當前協(xié)程, 等待發(fā)送數據 gp := getg() mysg := acquireSudog() mysg.releasetime = 0 if t0 != 0 { mysg.releasetime = -1 } mysg.elem = ep mysg.waitlink = nil mysg.g = gp mysg.isSelect = false mysg.c = c gp.waiting = mysg gp.param = nil c.sendq.enqueue(mysg) gopark(chanparkcommit, unsafe.Pointer(&c.lock), waitReasonChanSend, traceEvGoBlockSend, 2) KeepAlive(ep) if mysg != gp.waiting { throw("G waiting list is corrupted") } gp.waiting = nil gp.activeStackChans = false if gp.param == nil { if c.closed == 0 { throw("chansend: spurious wakeup") } panic(plainError("send on closed channel")) } gp.param = nil if mysg.releasetime > 0 { blockevent(mysg.releasetime-t0, 2) } mysg.c = nil releaseSudog(mysg) return true }
雖然最終阻塞的地方沒看太明白, 不過發(fā)送數據的大體流程很清楚:
- 若無需阻塞且不能發(fā)送數據, 返回失敗
- 若存在接收者, 直接發(fā)送數據
- 若存在緩存, 將數據放到緩存中
- 若無需阻塞, 返回失敗
- 阻塞等待發(fā)送數據
其中不加鎖的操作, 在看到selectnbsend
函數的注釋時如下:
// compiler implements // // select { // case c <- v: // ... foo // default: // ... bar // } // // as // // if selectnbsend(c, v) { // ... foo // } else { // ... bar // } // func selectnbsend(c *hchan, elem unsafe.Pointer) (selected bool) { return chansend(c, elem, false, getcallerpc()) }
看這意思, select
關鍵字有點類似于語法糖, 其內部會轉換成調用selectnbsend
函數的簡單if
判斷.
接收數據
至于接收數據的方法, 其內部實現(xiàn)與發(fā)送大同小異. runtime.chanrecv
方法.
源碼簡單看了一下, 雖理解不深, 但對channel
也有了大體的認識.
上手
簡單對channel
的使用總結一下.
定義
// 創(chuàng)建普通的管道類型, 非緩沖 a := make(chan int) // 創(chuàng)建緩沖區(qū)大小為10的管道 b := make(chan int, 10) // 創(chuàng)建只用來發(fā)送的管道 c := make(chan<- int) // 創(chuàng)建只用來接收的管道 d := make(<-chan int) // eg: 只用來接收的管道, 每秒一個 e := time.After(time.Second)
發(fā)送與接收
// 接收數據 a := <- ch b, ok := <- ch // 發(fā)送數據 ch <- 2
最后, 看了一圈, 感覺channel
并不是很復雜, 就是一個隊列, 一端接受, 一端發(fā)送. 不過其對多協(xié)程處理做了很多優(yōu)化. 與協(xié)程配合, 靈活使用的話, 應該會有不錯的效果.
到此這篇關于深入理解golang chan的使用的文章就介紹到這了,更多相關golang chan內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
golang微服務框架kratos實現(xiàn)Socket.IO服務的方法
本文主要介紹了golang微服務框架kratos實現(xiàn)Socket.IO服務的方法,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2023-06-06Golang實現(xiàn)http server提供壓縮文件下載功能
這篇文章主要介紹了Golang實現(xiàn)http server提供壓縮文件下載功能,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2021-01-01