golang chan 探究
前言
之前在看golang多線程通信的時(shí)候, 看到了go 的管道. 當(dāng)時(shí)就覺得這玩意很神奇, 因?yàn)橹敖佑|過的不管是php,?java,?Python,?js,?c等等, 都沒有這玩意, 第一次見面, 難免勾起我的好奇心. 所以就想著看一看它具體是什么東西. 很明顯, 管道是go實(shí)現(xiàn)在語言層面的功能, 所以我以為需要去翻他的源碼了. 雖然最終沒有翻到C的層次, 不過還是受益匪淺.
見真身
結(jié)構(gòu)體
要想知道他是什么東西, 沒什么比直接看他的定義更加直接的了. 但是其定義在哪里么? 去哪里找呢? 還記得我們是如何創(chuàng)建chan的么??make方法. 但是當(dāng)我找過去的時(shí)候, 發(fā)現(xiàn)make方法只是一個(gè)函數(shù)的聲明.
這, 還是沒有函數(shù)的具體實(shí)現(xiàn)啊. 匯編看一下. 編寫以下內(nèi)容:
package mainfunc main() {_ = make(chan int) }執(zhí)行命令:
go tool compile -N -l -S main.go
雖然匯編咱看不懂, 但是其中有一行還是引起了我的注意.
make調(diào)用了runtime.makechan. 漂亮, 就找他.
找到他了, 是hchan指針對(duì)象. 整理了一下對(duì)象的字段(不過人家自己也有注釋的):
// 其內(nèi)部維護(hù)了一個(gè)循環(huán)隊(duì)列(數(shù)組), 用于管理發(fā)送與接收的緩存數(shù)據(jù). type hchan struct {// 隊(duì)列中元素個(gè)數(shù)qcount uint// 隊(duì)列的大小(數(shù)組長(zhǎng)度)dataqsiz uint// 指向底層的緩存隊(duì)列, 是一個(gè)可以指向任意類型的指針. buf unsafe.Pointer// 管道每個(gè)元素的大小elemsize uint16// 是否被關(guān)閉了closed uint32// 管道的元素類型elemtype *_type// 當(dāng)前可以發(fā)送的元素索引(隊(duì)尾)sendx uint // 當(dāng)前可以接收的元素索引(隊(duì)首)recvx uint // 當(dāng)前等待接收數(shù)據(jù)的 goroutine 隊(duì)列recvq waitq// 當(dāng)前等待發(fā)送數(shù)據(jù)的 goroutine 隊(duì)列sendq waitq // 鎖, 用來保證管道的每個(gè)操作都是原子性的. lock mutex }可以看的出來, 管道簡(jiǎn)單說就是一個(gè)隊(duì)列加一把鎖.
發(fā)送數(shù)據(jù)
依舊使用剛才的方法分析, 發(fā)送數(shù)據(jù)時(shí)調(diào)用了runtime.chansend1?函數(shù). 其實(shí)現(xiàn)簡(jiǎn)單易懂:
然后查看真正實(shí)現(xiàn), 函數(shù)步驟如下(個(gè)人理解, 有一些 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, 應(yīng)該是開發(fā)時(shí)調(diào)試用的. if debugChan {print("chansend: chan=", c, "\n")}// 常量, 恒為 false, 沒看懂這個(gè)判斷if raceenabled {racereadpc(c.raceaddr(), callerpc, funcPC(chansend))}// 若當(dāng)前操作不阻塞, 且管道還沒有關(guān)閉時(shí)判斷// 當(dāng)前隊(duì)列容量為0且沒有等待接收數(shù)據(jù)的 或 當(dāng)前隊(duì)列容量不為0且隊(duì)列已滿// 那么問題來了, 什么時(shí)候不加鎖呢? select 的時(shí)候. 可以在不阻塞的時(shí)候快速返回if !block && c.closed == 0 && ((c.dataqsiz == 0 && c.recvq.first == nil) ||(c.dataqsiz > 0 && c.qcount == c.dataqsiz)) {return false}// 上鎖, 保證操作的原子性lock(&c.lock)// 若管道已經(jīng)關(guān)閉, 報(bào)錯(cuò)if c.closed != 0 {unlock(&c.lock)panic(plainError("send on closed channel"))}// 從接受者隊(duì)列獲取一個(gè)接受者, 若存在, 數(shù)據(jù)直接發(fā)送, 不走緩存, 提高效率if sg := c.recvq.dequeue(); sg != nil {send(c, sg, ep, func() { unlock(&c.lock) }, 3)return true}// 若緩存為滿, 則將數(shù)據(jù)放到緩存中排隊(duì)if c.qcount < c.dataqsiz {// 取出對(duì)尾的地址qp := chanbuf(c, c.sendx)// 將ep 的內(nèi)容拷貝到 ap 地址typedmemmove(c.elemtype, qp, ep)// 更新隊(duì)尾索引c.sendx++if c.sendx == c.dataqsiz {c.sendx = 0}c.qcount++unlock(&c.lock)return true}// 若當(dāng)前不阻塞, 直接返回if !block {unlock(&c.lock)return false}// 當(dāng)走到這里, 說明數(shù)據(jù)沒有成功發(fā)送, 且需要阻塞等待. // 以下代碼沒看懂, 不過可以肯定的是, 其操作為阻塞當(dāng)前協(xié)程, 等待發(fā)送數(shù)據(jù)gp := getg()mysg := acquireSudog()mysg.releasetime = 0if t0 != 0 {mysg.releasetime = -1}mysg.elem = epmysg.waitlink = nilmysg.g = gpmysg.isSelect = falsemysg.c = cgp.waiting = mysggp.param = nilc.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 = nilgp.activeStackChans = falseif gp.param == nil {if c.closed == 0 {throw("chansend: spurious wakeup")}panic(plainError("send on closed channel"))}gp.param = nilif mysg.releasetime > 0 {blockevent(mysg.releasetime-t0, 2)}mysg.c = nilreleaseSudog(mysg)return true }雖然最終阻塞的地方?jīng)]看太明白, 不過發(fā)送數(shù)據(jù)的大體流程很清楚:
其中不加鎖的操作, 在看到selectnbsend函數(shù)的注釋時(shí)如下:
// 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關(guān)鍵字有點(diǎn)類似于語法糖, 其內(nèi)部會(huì)轉(zhuǎn)換成調(diào)用selectnbsend函數(shù)的簡(jiǎn)單if判斷.
接收數(shù)據(jù)
至于接收數(shù)據(jù)的方法, 其內(nèi)部實(shí)現(xiàn)與發(fā)送大同小異.?runtime.chanrecv?方法.
源碼簡(jiǎn)單看了一下, 雖理解不深, 但對(duì)channel也有了大體的認(rèn)識(shí).
上手
簡(jiǎn)單對(duì)channel的使用總結(jié)一下.
定義
// 創(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: 只用來接收的管道, 每秒一個(gè) e := time.After(time.Second)發(fā)送與接收
// 接收數(shù)據(jù) a := <- ch b, ok := <- ch // 發(fā)送數(shù)據(jù) ch <- 2最后, 看了一圈, 感覺channel并不是很復(fù)雜, 就是一個(gè)隊(duì)列, 一端接受, 一端發(fā)送. 不過其對(duì)多協(xié)程處理做了很多優(yōu)化. 與協(xié)程配合, 靈活使用的話, 應(yīng)該會(huì)有不錯(cuò)的效果.
創(chuàng)作挑戰(zhàn)賽新人創(chuàng)作獎(jiǎng)勵(lì)來咯,堅(jiān)持創(chuàng)作打卡瓜分現(xiàn)金大獎(jiǎng)總結(jié)
以上是生活随笔為你收集整理的golang chan 探究的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: contiki cooja仿真
- 下一篇: 补码到底是个什么东西