小公尺大佬走進 Go 之 Channel 的使用

2022-09-21 20:12:12 字數 3869 閱讀 2647

以下文章**於大愚talk ,作者大愚talk

對於 golang 語言應用層面的知識,先講如何正確的使用,然後再講它的實現。

don't communicate by sharing memory, share memory by communicating.

相信寫過 go 的同學都知道這句名言,可以說 channel 就是後邊這句話的具體實現。我們來看一下到底 channel 是什麼?

channel 是乙個型別安全的佇列(迴圈佇列),能夠控制 groutine 在它上面讀寫訊息的行為,比如:阻塞某個 groutine ,或者喚醒某個 groutine。

不同的 groutine 可以通過 channel 交換任意的資源,由於 channel 能夠控制 groutine 的行為,所以 csp 模型才能在 golang 中順利實現,它確保了不同 groutine 之間的資料同步機制。

上面的話是不是聽起來非常的不舒服?

好吧,簡單說人話就是,channel 是用來在 不同的 的 goroutine 中交換資料的。一定要注意這裡 不同的 三個字。千萬不要把 channel 拿來在不同函式(同乙個 goroutine 中)間交換資料。

知道了定義,我們來看具體如何使用。

如何定義乙個 channel 型別呢?

var ch1 chan int // 定義了乙個 int 型別的 channel,沒有初始化,是 nil

ch2 := make(chan int) // 定義+初始化了乙個無緩衝的 int 型別 channel

ch3 := make(chan int) // 定義+初始化了乙個有緩衝的 int 型別 channel

上面的定義方法我們都是定義的雙向通道,對應的還有單向通道,但是單向通道我們一般只是做為函式引數來進行一些限制,並不會在定義、初始化時就搞乙個單向通道出來。因為你定義乙個單向通道沒有任何實際價值,通道的存在本來就是用來交換資料的,單向通道只能滿足發或者收。

下面我們一起來看一下具體的使用,以及使用中注意的一些點。

不管是有緩衝的通道還是無緩衝的通道都是用來交換資料的,既然是交換資料,無非就是寫入、讀取。我們先從傳送開始。

ch := make(chan int)

defer close(ch)

//ch

go func(ch chan int) (ch)

// ch

如果我們開啟 位置一 的注釋,程式是無法獲得預期執行的,由於該 channel 是無緩衝的,位置一的**會陷入阻塞,下一行的 goroutine 根本沒有機會執行。整個**會陷入死鎖。

正確的操作是,開啟 位置二 的注釋,因為上一行 goroutine 先行啟動,他是乙個獨立的協程,不會阻塞主 groutine 的執行。但它內部會阻塞在 num :=

這裡先提一點,無緩衝的 channel 並不會用到內部結構體的 buf ,這部分具體會在原始碼部分講解他們的資料訪問、交換的方式。

ch := make(chan int, 1) // 注意這裡

defer close(ch)

//ch

go func(ch chan int) (ch)

// ch

**基本沒有改變,唯一的區別是 make 函式傳入了第二個引數,這個值的含義是緩衝的大小。那麼此時 位置一 與 位置二 都能夠正常執行嗎?

答案是肯定的,此時的**,無論是那個位置,開啟注釋後都能夠正常執行。原因就在於由於 channel 有了快取區域,位置一 寫入資料不會造成主協程的阻塞,那麼下一行**的子協程就可以正常啟動,並直接將位置一寫入 buf 的資料讀取出來列印。

對於 位置二 ,由於子協程先啟動,但是會被阻塞在 num :=

傳送需要注意幾個問題:

什麼時候會被阻塞?向 nil 通道傳送資料會被阻塞向無緩衝 channel 寫資料,如果讀協程沒有準備好,會阻塞向有緩衝 channel 寫資料,如果緩衝已滿,會阻塞什麼時候會 panic?closed的 channel,寫資料會 panic就算是有緩衝的 channel ,也不是每次傳送、接收都要經過快取,如果傳送的時候,剛好有等待接收的協程,那麼會直接交換資料。

有寫入,必然後讀取。

還是上面的**, num :=

這裡說下讀取的兩種形式。

形式一multi-valued assignment

v, ok := <-ch

ok 是乙個 bool 型別,可以通過它來判斷 channel 是否已經關閉,如果關閉該值為 true ,此時 v 接收到的是 channel 型別的零值。比如:channel 是傳遞的 int, 那麼 v 就是 0 ;如果是結構體,那麼 v 就是結構體內部對應欄位的零值。

形式二v := <-ch

該方式對於關閉的 channel 無法掌控,我們示例中就是該種方式。

接收需要注意幾個問題:

什麼時候會被阻塞?從 nil 通道接收資料會被阻塞從無緩衝 channel 讀資料,如果寫協程沒有準備好,會阻塞從有緩衝 channel 讀資料,如果緩衝為空,會阻塞讀取的 channel 如果被關閉,並不會影響正在讀的資料,它會將所有資料讀取完畢,並不會立即就失敗或者返回零值

對於 channel 的關閉,在什麼地方去關閉呢?因為上面也講到向 closed 的 channel 寫或者繼續 close 都會導致 panic問題。

一般的建議是誰寫入,誰負責關閉。如果涉及到多個寫入的協程、多個讀取的協程?又該如何關閉?總的來說就是加入乙個標記避免重複關閉。不過真的不建議搞的太複雜,否則後續維護**會瘋掉。

關閉需要注意幾個問題:

什麼時候會 panic?closed 的 channel,再次關閉 close 會 panic

我們常常會用 for-range 來讀取 channel

的資料。

ch := make(chan int, 1)

go func(ch chan int) (ch)

for val := range ch (ch, q)

fibonacci := func(ch, q chan int) {

x, y := 0, 1

for {

select {

case ch

x, y = y, x+y

break // 你覺得是否會影響 for 語句的迴圈?

case

fmt.println("quit")

return

fibonacci(ch, q)

上面的**是利用 channel 實現的乙個斐波拉契數列。select 還可以有 default 語句,該語句會在其它 case 都被阻塞的情況下執行。

關注的問題

select 只要有預設語句,就不會被阻塞,換句話說,如果沒有 default,然後 case 又都不能讀或者寫,則會被阻塞nil 的 channel,不管讀寫都會被阻塞select 不能夠像 for-range 一樣發現 channel 被關閉而終止執行,所以需要結合 multi-valued assignment 來處理如果同時有多個 case 滿足了條件,會使用偽隨機選擇乙個 case 來執行select 語句如果不配合 for 語句使用,只會對 case 表示式求值一次每次 select 語句的執行,是會掃碼完所有的 case 後才確定如何執行,而不是說遇到合適的 case 就直接執行了。

本文幾個重要問題再次總結下,也是經常面試的常考點。

向 close 的 channel 寫資料、再次 close 都會觸發 runtime panic。向 nil channel 寫、讀取資料,都會阻塞,可以利用這點來優化 for + select 的用法。channel 的關閉最好在寫入方處理,讀的協程不要去關閉 channel,可以通過單向通道來表明 channel 在該位置的功能。如果有多個寫協程的 channel 需要關閉,可以使用額外的 channel 來標記,也可以使用 sync.once 或者 sync.mutex 來處理。channel 不管是讀寫都是併發安全的,不會出現多個協程同時讀或者寫的情況,從而實現了 csp。

如何區分真大佬和偽大佬

中國大概99 的都是偽大佬吧,偽大佬自稱神,自稱巨佬,自稱遠古巨神等等,但是說句實話,大部分恐怕都沒接觸過大佬圈。要區分很簡單 偽大佬很喜歡自我抬高,每天到處寫部落格發自己的東西,顯示自己實力高強 真大佬最怕自我抬高,因為到處來一幫想白嫖的 偽大佬閒,閒的體現就是大量的時間做宣傳自己的事情 真大佬你...

科技大佬走進直播間秀的是什麼?蘇寧真是6得不行!

在科技界,各大佬向來都是以低調著稱,保持神秘感是他們一貫的做法。結果,蘇寧一下就把國內手機界各大巨頭的老總們拉下 水 平時深居簡出 一心撲到經營上的boss們褪去企業家光環,紛紛在蘇寧直播平台開啟了直播,與各路粉絲面對面交流。為何蘇寧這麼受到企業家們的歡迎呢?原來在這炎炎夏日,蘇寧搞了乙個818發燒...

大佬的難題

給n個三維座標點,滿足每維座標都是1 n的排列,求三維偏序。注意到任意兩個位置,都有乙個位置有至少兩維比另乙個位置的對應兩維大,於是可以容斥,那麼只需要做二維偏序。include include define fo i,a,b for i a i b i using namespace std ty...