golang-channel

什麼是 channel 管道

channel 是 goroutine 與 goroutine 之間通信的重要橋樑

channel 是一個通道,用於端到端的數據傳輸,這有點像我們平常使用的消息隊列,只不過 channel 的發送方和接受方是 goroutine 對象,屬於內存級別的通信。

這裏涉及到了 goroutine 概念,goroutine 是輕量級的協程,有屬於自己的棧空間。 我們可以把它理解爲線程,只不過 goroutine 的性能開銷很小,並且在用戶態上實現了屬於自己的調度模型。

傳統的線程通信有很多方式,像內存共享、信號量等。其中內存共享實現較爲簡單,只需要對變量進行併發控制,加鎖即可。但這種在後續業務逐漸複雜時,將很難維護,耦合性也比較強。

後來提出了 CSP 模型,即在通信雙方抽象出中間層,數據的流轉由中間層來控制,通信雙方只負責數據的發送和接收,從而實現了數據的共享,這就是所謂的通過通信來共享內存。 channel 就是按這個模型來實現的。

channel 在多併發操作裏是屬於協程安全的,並且遵循了 FIFO 特性。即先執行讀取的 goroutine 會先獲取到數據,先發送數據的 goroutine 會先輸入數據。

另外,channel 的使用將會引起 Go runtime 的調度調用,會有阻塞和喚起 goroutine 的情況產生。

channel特性特性

1、channel,可譯爲通道,是go語言協程goroutine之間的通信方式。
2、channel通信可以想象成從管道的一頭塞進數據,從另一頭讀取數據。
3、協程通過channel通信可以不用進行加鎖操作。
4、把數據發往無緩衝通道,如果接收方沒有接收。發送操作將持續阻塞,此時會 釋放cpu,執行其他協程,並且查看其他攜程是否能夠解除阻塞
5、接收將持續阻塞直到發送方發送數據

channel的2種類型

c1 := make(chan int) // 無緩衝
c2 := make(chan interface{}) // 任意類型通道
c3 := make(chan int, 1) // 有緩衝

type Str struct{}
c4 := make(chan *Str) // 指針類型通道
c5 := make(chan struct{})

無緩衝

ch := make(chan int)

上面是創建了無緩衝的 channel,一旦有 goroutine 往 channel 發送數據,那麼當前的 goroutine 會被阻塞住,直到有其他的 goroutine 消費了 channel 裏的數據,才能繼續運行。

有緩衝

ch := make(chan int, 2)

第二個參數表示 channel 可緩衝數據的容量。只要當前 channel 裏的元素總數不大於這個可緩衝容量,則當前的 goroutine 就不會被阻塞住。

阻塞條件:

  1. 通道被填滿時,嘗試再次發送數據發生阻塞
  2. 通道中沒數據時,嘗試接收數據時會發生阻塞

nil的channel

我們也可以聲明一個 nil 的 channel,只是創建這樣的 channel 沒有意義,讀、寫 channel 都將會被阻塞住。一般 nil channel 用在 select 上,讓 select 不再從這個 channel 裏讀取數據,

func main() {
    ch1 := make(chan int)
    ch2 := make(chan int)

    go func() {
        for i := 0; i < 3; i++ {
            time.Sleep(1 * time.Second)
            ch1 <- 2
        }
        fmt.Println("ch1  set null")
        //模擬某些業務場景寫入nil
        ch1 = nil
    }()

    go func() {
        for {
            time.Sleep(1 * time.Second)
            ch2 <- 2
        }
    }()

    for {
        select {
        case <-ch1: // 當 ch1 被設置爲 nil 後,將不會到達此分支了。
            fmt.Println("ch1 process")
        case <-ch2:
            fmt.Println("ch2 process")
        }
    }
}

輸出

ch1 process
ch2 process
ch1 process
ch2 process
ch1  set null
ch1 process
ch2 process
ch2 process
ch2 process
ch2 process
ch2 process
ch2 process
ch2 process
ch2 process
ch2 process
ch2 process
ch2 process
ch2 process
ch2 process
ch2 process
Exiting.
View Code

channel三種模式

只讀

只寫

channel三種狀態

使用例子

創建

帶緩衝
ch := make(chan int,3)
不帶緩衝
ch := make(chan int)

創建時會做一些檢查:

元素大小不能超過64K
元素對齊大小不能超過maxAlign(8字節)
計算出來的內存是否超過限制
創建時的策略:

無緩衝的channel——會直接給hchan分配內存
有緩衝的channel並且元素不包含指針(buf指針,指向底層數組)——會爲hchan和底層數組分配一段連續的地址
有緩衝的channel並且元素包含指針——會爲hchan和底層數組分別分配地址

發送

包括檢查和數據發送兩個步驟

數據發送步驟

1.如果channel的讀等待隊列存在接收者goroutine(有發送者goroutine阻塞)

將數據直接發送給第一個等待的goroutine,喚醒接收的goroutine

2.如果channel的讀等待隊列不存在接收者goroutine(無有發送者goroutine阻塞)

如果buf指向的循環數組未滿,會把數據發送到循環數組的隊尾

如果buf指向的循環數組已滿,就會阻塞,將當前goroutine加入寫等待隊列,並掛起等待喚醒

func chansend(c *hchan,ep unsafe.Pointer,block bool,callerpc uintptr)bool

阻塞式

ch <- 10

非阻塞式

select {
    case ch <- 10:
    ...
  default

select中的default 會導致select無阻塞,也會導致cpu飆高問題

去掉default,select會阻塞,直到通道有數據時解除

當兩個通道同時有數據產生時,選擇其中一個通道去執行,直到所有通道數據都處理完畢

接收

包括檢查和數據接收兩個步驟

數據接收步驟

1.如果channel的寫等待隊列存在發送者goroutine(有發送者goroutine阻塞)

如果是無緩衝channel,直接從第一個發送者goroutine那裏把數據拷貝給接收變量,喚醒發送的goroutine

如果是有緩衝channel(已滿),將循環數組buf的隊首元素拷貝給接收變量,將第一個發送者goroutine的數據拷貝到buf指向的循環數組隊尾,喚醒發送的goroutine

2.如果channel的寫等待隊列不存在發送者goroutine(沒有發送者goroutine阻塞)

如果buf指向的循環數組非空,將循環數組的隊首元素拷貝給接收變量

如果buf指向的循環數組爲空,這個時候就會阻塞,將當前goroutine加入讀等待隊列,並掛起等待喚醒
阻塞式

<-ch
 
v:= <-ch
 
v,ok := <-ch
 
//當channel關閉時,for循環會自動退出,無需主動監測channel是否關閉,可以防止讀取已經關閉的channel,造成讀到數據爲通道所存儲的數據類型的零值
for i := range ch {
    fmt.Println(i)
}

非阻塞式

select {
    case  <- ch:
    ...
  default
}

select中的default 會導致select無阻塞,也會導致cpu飆高問題

去掉default,select會阻塞,直到通道有數據時解除

當兩個通道同時有數據產生時,選擇其中一個通道去執行,直到所有通道數據都處理完畢

channel中的死鎖

例子一

無緩衝的channel的讀寫者必須同時完成發送和接收,而不能串行,顯然單協程無法滿足。所以這裏造成了循環等待,會死鎖。

func main(){
    ch := make(chan int)
    ch <- 1
    <- ch
}

 

例子二

往 channel 裏讀寫數據時是有可能被阻塞住的,一旦被阻塞,則需要其他的 goroutine 執行對應的讀寫操作,才能解除阻塞狀態。

然而,阻塞後一直沒能發生調度行爲,沒有可用的 goroutine 可執行,則會一直卡在這個地方,程序就失去執行意義了。此時 Go 就會報 deadlock 錯誤,如下代碼:
讀channel和寫channel都需要出現,單獨出現會死鎖

func main(){
    ch := make(chan int)
    ch <- 1
}
func main(){
    ch := make(chan int)
    <-ch
}

channel底層原理

channel 創建後返回了 hchan 結構體

type hchan struct {
    qcount   uint   // channel 裏的元素計數
    dataqsiz uint   // 可以緩衝的數量,如 ch := make(chan int, 10)。 此處的 10 即 dataqsiz
    elemsize uint16 // 要發送或接收的數據類型大小
    buf      unsafe.Pointer // 當 channel 設置了緩衝數量時,該 buf 指向一個存儲緩衝數據的區域,該區域是一個循環隊列的數據結構
    closed   uint32 // 關閉狀態
    sendx    uint  // 當 channel 設置了緩衝數量時,數據區域即循環隊列此時已發送數據的索引位置
    recvx    uint  // 當 channel 設置了緩衝數量時,數據區域即循環隊列此時已接收數據的索引位置
    recvq    waitq // 想讀取數據但又被阻塞住的 goroutine 隊列
    sendq    waitq // 想發送數據但又被阻塞住的 goroutine 隊列

    lock mutex //同步鎖-互斥鎖
    ...
}
recvq和sendq分別是接收(<-channel)或者發送(channel <- xxx)的goroutine抽象出來的結構體(sudog)的隊列。是個雙向鏈表

無緩衝channel先寫再讀

1.G1往channel寫數據,由於 channel 是無緩衝的,所以 G1 暫時被掛在 sendq 隊列裏,然後 G1 調用了 gopark 休眠了起來。

2.g2從隊列裏讀數據

G2 發現 sendq 等待隊列裏有 goroutine 存在,於是直接從 G1 copy 數據過來,並且會對 G1 設置 goready 函數,這樣下次調度發生時, G1 就可以繼續運行,並且會從等待隊列裏移除掉。

注意:緩存鏈表中以上每一步的操作,都是需要加鎖操作的!

  • 每一加粗樣式步的操作的細節可以細化爲:
  • 第一,加鎖
  • 第二,把數據從goroutine中copy到“隊列”中(或者從隊列中copy到goroutine中)
  • 第三,釋放鎖

無緩衝channel 先讀再寫

1.G1讀取時發現sendq沒有goroutline存在,G1 暫時被掛在了 recvq 隊列,然後休眠起來。

 2.G2 在寫數據時,發現 recvq 隊列有 goroutine 存在,於是直接將數據發送給 G1。同時設置 G1 goready 函數,等待下次調度運行。

有緩衝channel先寫再讀

1.優先判斷緩衝數據區域是否已滿,如果未滿,則將數據保存在緩衝數據區域,即環形隊列裏。如果已滿,則和之前的流程是一樣的。

 

 2.當 G2 要讀取數據時,會優先從緩衝數據區域去讀取,並且在讀取完後,會檢查 sendq 隊列,如果 goroutine 有等待隊列,則會將它上面的 data 補充到緩衝數據區域,並且也對其設置 goready 函數。

有緩存先讀後寫

流程一樣

使用注意事項

寫數據注意

向一個nil channel發送數據,會調用gopark函數將當前goroutine掛起
向一個已經關閉的channel發送數據,直接會panic
如果channel的recvq當前隊列中有被阻塞的接收者,則直接將數據發送給當前goroutine
當channel的緩衝區還有空閒空間,則將數據發送到sendx指向緩衝區的位置
當沒有緩衝區或者緩衝區滿了,則會創建一個sudog的結構體將其放到channel的sendq隊列當中陷入休眠等待被喚醒

讀數據注意

從一個nil channel接收數據,會調用gopark函數將當前goroutine掛起,讓出處理器的使用權
從一個已經關閉並且緩衝區中沒有元素的channel中接收數據,則會接收到該類型的默認元素,並且第二個返回值返回false
如果channel沒有緩衝區且sendq的隊列有阻塞的goroutine,則把sendq隊列頭的sudog中保存的元素值copy到目標地址中
如果channel有緩衝區且緩衝區裏面有元素,則把recvx指向緩衝區的元素值copy到目標地址當中,sendq隊列頭的sudog的元素值copy到recvx指向緩衝區位置的地址當中
當上面的條件都不符合時,則會創建一個sudog的結構體將其放到channel的recvq隊列當中陷入休眠等待被喚醒

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章