golang chan 探究

2020-10-11 菸草的香味

前言

之前在看golang多線程通信的時候, 看到了go 的管道. 當時就覺得這玩意很神奇, 因為之前接觸過的不管是php, java, Python, js, c等等, 都沒有這玩意, 第一次見面, 難免勾起我的好奇心. 所以就想著看一看它具體是什麼東西. 很明顯, 管道是go實現在語言層面的功能, 所以我以為需要去翻他的源碼了. 雖然最終沒有翻到C的層次, 不過還是受益匪淺.

見真身

結構體

要想知道他是什麼東西, 沒什麼比直接看他的定義更加直接的了. 但是其定義在哪裡麼? 去哪裡找呢? 還記得我們是如何創建chan的麼? make方法. 但是當我找過去的時候, 發現make方法只是一個函數的聲明.

這, 還是沒有函數的具體實現啊. 彙編看一下. 編寫以下內容:

package mainfunc main() { _ = make(chan int)}

執行命令:

go tool compile -N -l -S main.go

雖然彙編咱看不懂, 但是其中有一行還是引起了我的注意.

make調用了runtime.makechan. 漂亮, 就找他.

找到他了, 是hchan指針對象. 整理了一下對象的欄位(不過人家自己也有注釋的):

// 其內部維護了一個循環隊列(數組), 用於管理髮送與接收的緩存數據. type hchan struct { // 隊列中元素個數 qcount uint // 隊列的大小(數組長度) dataqsiz uint // 指向底層的緩存隊列, 是一個可以指向任意類型的指針. buf unsafe.Pointer // 管道每個元素的大小 elemsize uint16 // 是否被關閉了 closed uint32 // 管道的元素類型 elemtype *_type // 當前可以發送的元素索引(隊尾) sendx uint // 當前可以接收的元素索引(隊首) recvx uint // 當前等待接收數據的 goroutine 隊列 recvq waitq // 當前等待發送數據的 goroutine 隊列 sendq waitq // 鎖, 用來保證管道的每個操作都是原子性的. lock mutex}

可以看的出來, 管道簡單說就是一個隊列加一把鎖.

發送數據

依舊使用剛才的方法分析, 發送數據時調用了runtime.chansend1 函數. 其實現簡單易懂:

然後查看真正實現, 函數步驟如下(個人理解, 有一些 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, 應該是開發時調試用的. 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")) } // 從接受者隊列獲取一個接受者, 若存在, 數據直接發送, 不走緩存, 提高效率 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 } // 當走到這裡, 說明數據沒有成功發送, 且需要阻塞等待. // 以下代碼沒看懂, 不過可以肯定的是, 其操作為阻塞當前協程, 等待發送數據 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}

雖然最終阻塞的地方沒看太明白, 不過發送數據的大體流程很清楚:

  1. 若無需阻塞且不能發送數據, 返回失敗
  2. 若存在接收者, 直接發送數據
  3. 若存在緩存, 將數據放到緩存中
  4. 若無需阻塞, 返回失敗
  5. 阻塞等待發送數據

其中不加鎖的操作, 在看到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判斷.

接收數據

至於接收數據的方法, 其內部實現與發送大同小異. runtime.chanrecv 方法.

源碼簡單看了一下, 雖理解不深, 但對channel也有了大體的認識.

上手

簡單對channel的使用總結一下.

定義

// 創建普通的管道類型, 非緩衝a := make(chan int)// 創建緩衝區大小為10的管道b := make(chan int, 10)// 創建只用來發送的管道c := make(chan<- int)// 創建只用來接收的管道d := make(<-chan int)// eg: 只用來接收的管道, 每秒一個e := time.After(time.Second)

發送與接收

// 接收數據a := <- chb, ok := <- ch// 發送數據ch <- 2


最後, 看了一圈, 感覺channel並不是很複雜, 就是一個隊列, 一端接受, 一端發送. 不過其對多協程處理做了很多優化. 與協程配合, 靈活使用的話, 應該會有不錯的效果.

相關焦點

  • golang開發優化技巧
    for && rangerange:適合用在map,chan,slice區別:在range開始迭代時就淺拷貝了一個副本,對數組來說,相當於拷貝了一個新的數組進行迭代,修改原數組不會影響被迭代數組。答案是先進後出 在1.14版本以後,defer幾乎是0開銷panci 和 recover作用:在golang當中不存在tye ... catch 異常處理邏輯。在golang當中使用defer, panic和recover來控制程序執行流程,藉此來達到處理異常的目的。
  • 淺談golang的sync包
    sync包sync包是 golang 一個官方的異步庫,提供了一些各種基礎的異步的實現,如互斥鎖等。比較經典的sync.Cond實現有etcd的 FIFO Scheduler 的實現:func NewFIFOScheduler() Scheduler { f := &fifo{ resume: make(chan
  • Golang協程並發的流水線模型
    背景最近由於性能問題,後端服務一直在做python到golang的遷移和重構。go語言精簡優雅,既有編譯型語言的嚴謹和高性能,又有解釋型語言的開發效率,出色的並發性能也是go區別於其他語言的一大特色。go的並發編程代碼雖然簡單,但重在其並發模型和流程的設計。所以這裡總結下golang協程並發常用的流水線模型。
  • golang開發:select多路選擇
    select 多路選擇select寫法上跟switch case的寫法基本一致,只不過golang ch := make(chan bool, 0) ch1 := make(chan bool, 0) select { case ret := <-ch: fmt.Println(ret) case ret := <-ch1: fmt.Println(ret) }如果ch和ch1都沒有通信數據發送,select就一直阻塞
  • Golang之流式編程
    int) go inputStream(ch) go outputStream(ch) c := make(chan os.Signal, 1) signal.Notify(c, syscall.SIGTERM, syscall.SIGINT) <-c}func inputStream(ch chan int) { count := 0 for { ch <- count time.Sleep
  • 2020-11-10:golang中的接口,類型不空?...
    2020-11-10:golang中的接口,類型不空,值為空,如何判斷是nil?福哥答案2020-11-10:reflect.ValueOf(接口變量).IsNil(),用這個即可判斷。對於值類型,會panic。兩種方法如下:1.異常判斷:recover捕獲。
  • Golang面試make和new的用法
    在golang中,make和new都分配內存,但是它們之間仍然存在一些差異。只有了解它們之間的差異,才能在適當的場合使用它們。簡而言之,new只是分配內存,而不初始化內存;make分配並初始化內存。所謂的初始化就是給一個類型賦一個初始值,例如,字符為空,整數為0,邏輯值為false。
  • Golang 並發數據衝突檢測器與並發安全
    當兩個協程goroutine同時訪問相同的共享變量,其中有一個執行了寫操作,或兩個都執行了寫操作,就會出現數據競爭問題,導致數據異常.詳情請參考Go內存模型詳解:https://golang.org/ref/mem如以下代碼由於並發訪問同一個map,存在數據衝突,一定概率導致數據異常,程序崩潰:package
  • 手把手教姐姐寫消息隊列(golang-channel實現)
    源碼地址:https://github.com/asong2020/Golang_Dream/tree/master/code_demo/queue本文作者:asong 原文地址:https://studygolang.com/articles/30780
  • Rabbitmq實戰golang實現
    github.io/post/rabbitmq%E5%AE%9E%E6%88%98golang%E5%AE%9E%E7%8E%B0/原文作者:Gopherzhang= nil { return err } forever := make(chan bool) go func() { for d := range data { fmt.Printf("Received a message: %s\n", d.Body) f(d.Body) } }() <-forever return nil}
  • client-go和golang源碼中的技巧
    下面就遇到的一些技術講解,首先看第一個:sets.String(k8s.io/apimachinery/pkg/util/sets/string.go)實現了對golang map的key的處理,如計算交集,併集等。
  • Golang 並發數據衝突檢測器(Data Race Detector)與並發安全
    當兩個協程goroutine同時訪問相同的共享變量,其中有一個執行了寫操作,或兩個都執行了寫操作,就會出現數據競爭問題,導致數據異常.詳情請參考Go內存模型詳解:https://golang.org/ref/mem如以下代碼由於並發訪問同一個map,存在數據衝突,一定概率導致數據異常,程序崩潰:package mainimport "fmt"
  • golang cron 定時任務
    day, midnight0 0 0 * * *@hourlyRun once an hour, beginning of hour0 0 * * * *@every <duration>every duration1c.AddFunc("@every 1h30m", func() { fmt.Println("Every hour thirty") })golang
  • golang學習筆記和部分特性源碼分析
    設計的目的是清理資源所以這樣) defer後面一定是一個函數 結構和函數一樣(棧地址 程序計數器 函數地址) 多一個指針 指向下個defer;goroutine也有一個defer指針 指向defer的單鍊表 return不是原子操作,執行過程是: 保存返回值(若有)—>執行defer(若有)—>執行ret跳轉,申請資源後立即使用defer關閉資源是好習慣 select golang
  • 當Golang遇到高性能的RabbitMQ ……
    RabbitMQ是出色的消息中間件,golang理所當然的也支持了。RabbitMQ是一個很棒的pub-sub系統,並且pub-sub已成為微服務中的主要通信體系結構。在我目前的工作中,我們每天通過Go服務使用RabbitMQ推送數億個社交媒體帖子。讓我們一起來看一下如何使用開源amqp軟體包有效地發布和訂閱消息 。
  • 暴走漫畫:4chan的混沌魅力
    這些暴走漫畫絕大多數來自4chan(4葉),一個擅長製造網際網路流行文化的社區。它由當時還是15歲的學生Moot(2008年才得知其真名叫克里斯多福·普爾)於2004年前後模仿日本著名論壇2chan(雙葉頻道)創辦,旨在打造一個美國版的2chan。該社區最大的特點就是不需註冊,直接匿名即可發布內容。這在引起很大爭議的同時,也讓4chan取得了巨大的成功。
  • Golang 反射
    35return2 36} 37 38funcmain() { 39var simpleObj Foo 40var pointer2obj = &simpleObj 41var simpleIntArray = [3]int{1, 2, 3} 42var simpleMap = map[string]string{ 43"a": "b", 44 } 45var simpleChan = make(chanint