<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          深入解讀Golang信道

          共 7314字,需瀏覽 15分鐘

           ·

          2022-07-10 23:57

          信道是一個golang goroutine之間很關(guān)鍵的通信媒介。

          理解golang的信道很重要,這里記錄平時易忘記的、易混淆的點(diǎn)。

          1. 基本使用

          剛聲明的信道,零值為nil,無法直接使用,需配合make函數(shù)進(jìn)行初始化

             ic :=  make(chan int)
             ic  <-22   // 向無緩沖信道寫入數(shù)據(jù)
             v := <-ic  // 從無緩沖信道讀取數(shù)據(jù)
          • 無緩沖信道:一手交錢,一手交貨, sender、receiver必須同時做好動作,才能完成發(fā)送->接收;否則,先準(zhǔn)備好的一方將會阻塞等待。
          • 有緩沖信道 make(chan int,10):滑軌流水線,因?yàn)榇嬖诰彌_空間,故并不強(qiáng)制sender、receiver必須同時準(zhǔn)備好;當(dāng)通道空或滿時, 有一方會阻塞。

          信道存在三種狀態(tài):nil, active, closed

          針對這三種狀態(tài),sender、receiver有一些行為,我也不知道如何強(qiáng)行記憶這些行為 ??:

          動作nilactiveclosed
          closepanic成功panic
          ch <-死鎖阻塞或成功panic
          <-ch死鎖阻塞或成功零值

          2. 從1個例子看chan的實(shí)質(zhì)

          package main
           
          import (
              "fmt"
          )
           
          func SendDataToChannel(ch chan int, value int) {
              fmt.Printf("ch's value:%v, chan's type: %T \n", ch, ch) // %v 顯示struct的值;%T 顯示類型
              ch <- value
          }
           
          func main() {
              var v int
              ch := make(chan int)     
              fmt.Printf("ch's value:%v, chan's type: %T \n", ch, ch) 
              go SendDataToChannel(ch, 101)         // 通過信道發(fā)送數(shù)據(jù)
              v = <-ch                              //  從信道接受數(shù)據(jù)
              fmt.Println(v)       // 101
          }

          能正確打印101。

          Q1:  剛學(xué)習(xí)golang的時候,一直給我們灌輸golang函數(shù)是值傳遞,那上例在另外一個協(xié)程內(nèi)部對形參的操作,為什么會影響外部的實(shí)參?

          請關(guān)注格式化字符的日志輸出:

          ch's value:0xc000018180, chan'type: chan int 
          ch's value:0xc000018180, chan'type: chan int 
          101

          A: 上面的日志顯示傳遞的ch是一個指針值0xc000018180,類型是chan int( 這并不是說ch是指向chan int類型的指針)。

          chan int本質(zhì)就是指向hchan結(jié)構(gòu)體的指針。

          內(nèi)置函數(shù)make[1]創(chuàng)建信道:func makechan(t *chantype, size int) *hchan返回了指向hchan結(jié)構(gòu)體的指針:

          type hchan struct {
           qcount   uint           // 隊(duì)列中已有的緩存元素的長度
           dataqsiz uint           // 環(huán)形隊(duì)列的長度
           buf      unsafe.Pointer // 環(huán)形隊(duì)列的地址
           elemsize uint16
           closed   uint32
           elemtype *_type // 元素類型
           sendx    uint   // 待發(fā)送的元素索引
           recvx    uint   // 待接受元素索引
           recvq    waitq  // 阻塞等待的goroutine
           sendq    waitq  // 阻塞等待的gotoutine

           // lock protects all fields in hchan, as well as several
           // fields in sudogs blocked on this channel.
           //
           // Do not change another G's status while holding this lock
           // (in particular, do not ready a G), as this can deadlock
           // with stack shrinking.
           lock mutex
          }

          Q2:緩沖信道內(nèi)部為什么要使用環(huán)形隊(duì)列?

          A:golang是使用數(shù)組來實(shí)現(xiàn)信道隊(duì)列,在不移動元素的情況下, 隊(duì)列會出現(xiàn)“假滿”的情況,

          在做成環(huán)形隊(duì)列的情況下, 所有的入隊(duì)出隊(duì)操作依舊是 O(1)的時間復(fù)雜度,同時元素空間可以重復(fù)利用。
          需要使用sendIndex,receIndex來標(biāo)記實(shí)際的待插入/拉取位置,顯而易見會出現(xiàn) sendIndex<=receIndex 的情況。

          recvq,receq是由鏈表實(shí)現(xiàn)的隊(duì)列,用于存儲阻塞等待的goroutine和待發(fā)送/待接收值, 這兩個結(jié)構(gòu)也是阻塞goroutine被喚醒的準(zhǔn)備條件。

          3. 發(fā)送/接收的細(xì)節(jié)

          不要使用共享內(nèi)存來通信,而是使用通信來共享內(nèi)存

          元素值從外界進(jìn)入信道會被復(fù)制,也就是說進(jìn)入信道的是元素值的副本,并不是元素本身進(jìn)入信道 (出信道類似)。

          金玉良言落到實(shí)處:不同的線程不共享內(nèi)存、不用鎖,線程之間通訊用channel同步也用channel。發(fā)送/接收數(shù)據(jù)的兩個動作(G1,G2,G3)沒有共享的內(nèi)存,底層通過hchan結(jié)構(gòu)體的buf,使用copy內(nèi)存的方式進(jìn)行通信,最后達(dá)到了共享內(nèi)存的目的。

          ②  根據(jù)第①點(diǎn),發(fā)送操作包括:復(fù)制待發(fā)送值,放置到信道內(nèi);
          接收操作包括:復(fù)制元素值, 放置副本到接收方,刪除原值,以上行為在全部完成之前都不會被打斷。所以第①點(diǎn)所說的無鎖,其實(shí)指的業(yè)務(wù)代碼無鎖,信道底層實(shí)現(xiàn)還是靠鎖。

          以send操作為例,下面代碼截取自 https://github.com/golang/go/blob/master/src/runtime/chan.go#L216

          if c.qcount < c.dataqsiz {
             // Space is available in the channel buffer. Enqueue the element to send.
             qp := chanbuf(c, c.sendx)         // 計(jì)算出buf中待插入位置的地址
             if raceenabled {
              racenotify(c, c.sendx, nil)
             }
             typedmemmove(c.elemtype, qp, ep)  // 將元素copy進(jìn)指定的qp地址
             c.sendx++                         // 重新計(jì)算待插入位置的索引
             if c.sendx == c.dataqsiz {
              c.sendx = 0                      
             }
             c.qcount++
             unlock(&c.lock)
             return true
            }

          一個常規(guī)的send動作:

          • 計(jì)算環(huán)形隊(duì)列的待插入位置的地址
          • 將元素copy進(jìn)指定的qp地址
          • 重新計(jì)算待插入位置的索引sendx
          • 如果待插入位置==隊(duì)列長度,說明插入位置已到尾部,需要插入首部。
          • 以上動作加鎖

          進(jìn)入等待狀態(tài)的goroutine會進(jìn)入hchan的sendq/recvq列表

          調(diào)度器將G1、G2置為waiting狀態(tài),G1、G2進(jìn)入sendq列表,同時與邏輯處理器分離;

          直到有G3嘗試讀取信道內(nèi)`recvx`元素[2],之后將喚醒[3]隊(duì)首G1[4]進(jìn)入runnable狀態(tài),加入調(diào)度器的runqueue。

          這里面涉及gopark, goready兩個函數(shù)。

          如果是無緩沖信道引起的阻塞,將會直接拷貝G1的待發(fā)送值到G2的存儲位置[5]

          ?? https://github.com/golang/go/blob/master/src/runtime/chan.go#L527

          package main

          import (
           "fmt"
           "time"
          )

          func SendDataToChannel(ch chan int, value int) {
           time.Sleep(time.Millisecond * time.Duration(value))
           ch <- value
          }

          func main() {
           var v int
           var ch chan int = make(chan int)
           go SendDataToChannel(ch, 104) // 通過信道發(fā)送數(shù)據(jù)
           go SendDataToChannel(ch, 100) // 通過信道發(fā)送數(shù)據(jù)
           go SendDataToChannel(ch, 95)  // 通過信道發(fā)送數(shù)據(jù)
           go SendDataToChannel(ch, 120) // 通過信道發(fā)送數(shù)據(jù)

           time.Sleep(time.Second)
           v = <-ch       //  從信道接受數(shù)據(jù)
           fmt.Println(v)  

           time.Sleep(time.Second * 10)
          }

          Q3:上述代碼大概率穩(wěn)定輸出95

          A:雖然4個goroutine被啟動的順序不定,但是肯定都阻塞了,阻塞的時機(jī)不一樣,被喚醒的是sendq隊(duì)首的goroutine,基本可認(rèn)為第三個goroutine被首先捕獲進(jìn)sendq ,因?yàn)槭菬o緩沖信道,將會直接拷貝G3的95給到待接收地址。

          4. 業(yè)內(nèi)總結(jié)的信道的常規(guī)姿勢

          無緩沖、緩沖信道的特征,已經(jīng)在golang領(lǐng)域形成了特定的套路。

          • 當(dāng)容量為0時,說明信道中不能存放數(shù)據(jù),在發(fā)送數(shù)據(jù)時,必須要求立馬有人接收,此時的信道稱之為無緩沖信道。

          • 當(dāng)容量為1時,說明信道只能緩存一個數(shù)據(jù),若信道中已有一個數(shù)據(jù),此時再往里發(fā)送數(shù)據(jù),會造成程序阻塞,利用這點(diǎn)可以利用信道來做鎖。

          • 當(dāng)容量大于1時,信道中可以存放多個數(shù)據(jù),可以用于多個協(xié)程之間的通信管道,共享資源。

          Q4:為什么無緩沖信道不適合做鎖?

          A:我們先思考一下鎖的業(yè)務(wù)實(shí)質(zhì):獲取獨(dú)占標(biāo)識,并能夠繼續(xù)執(zhí)行;無緩沖信道雖然可以獲取獨(dú)占標(biāo)識,但是他阻塞了自身goroutine的執(zhí)行,所以并不適合實(shí)現(xiàn)業(yè)務(wù)鎖。

          參考資料

          [1]

          內(nèi)置函數(shù)make: https://github.com/golang/go/blob/master/src/runtime/chan.go#L7

          [2]

          直到有G3嘗試讀取信道內(nèi)recvx元素: https://github.com/golang/go/blob/1ebc983000ed411a1c06f6b8a61770be1392e707/src/runtime/chan.go#L629

          [3]

          喚醒: https://github.com/golang/go/blob/1ebc983000ed411a1c06f6b8a61770be1392e707/src/runtime/chan.go#L654

          [4]

          隊(duì)首G1: https://github.com/golang/go/blob/1ebc983000ed411a1c06f6b8a61770be1392e707/src/runtime/chan.go#L527

          [5]

          如果是無緩沖信道引起的阻塞,將會直接拷貝G1的待發(fā)送值到G2的存儲位置: https://github.com/golang/go/blob/master/src/runtime/chan.go#L616


          瀏覽 68
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  免费观看成人毛片A片直播千姿 | 久久伊人AV | 亚洲AV成人片无码网站 | 国产乱伦免费视频 | 操屄的免费视频 |