無緩沖的通道(unbuffered channel)是指在接收前沒有能力保存任何值的通道。
創(chuàng)新互聯(lián)專業(yè)為企業(yè)提供進賢網(wǎng)站建設、進賢做網(wǎng)站、進賢網(wǎng)站設計、進賢網(wǎng)站制作等企業(yè)網(wǎng)站建設、網(wǎng)頁設計與制作、進賢企業(yè)網(wǎng)站模板建站服務,10年進賢做網(wǎng)站經(jīng)驗,不只是建網(wǎng)站,更提供有價值的思路和整體網(wǎng)絡服務。
這種類型的通道要求發(fā)送goroutine和接收goroutine同時準備好,才能完成發(fā)送和接收操作。否則,通道會導致先執(zhí)行發(fā)送或接收操作的 goroutine 阻塞等待。
這種對通道進行發(fā)送和接收的交互行為本身就是同步的。其中任意一個操作都無法離開另一個操作單獨存在。
阻塞:由于某種原因數(shù)據(jù)沒有到達,當前協(xié)程(線程)持續(xù)處于等待狀態(tài),直到條件滿足,才接觸阻塞。
同步:在兩個或多個協(xié)程(線程)間,保持數(shù)據(jù)內(nèi)容一致性的機制。
下圖展示兩個 goroutine 如何利用無緩沖的通道來共享一個值:
在第 1 步,兩個 goroutine 都到達通道,但哪個都沒有開始執(zhí)行發(fā)送或者接收。
在第 2 步,左側(cè)的 goroutine 將它的手伸進了通道,這模擬了向通道發(fā)送數(shù)據(jù)的行為。這時,這個 goroutine 會在通道中被鎖住,直到交換完成。
在第 3 步,右側(cè)的 goroutine 將它的手放入通道,這模擬了從通道里接收數(shù)據(jù)。這個 goroutine 一樣也會在通道中被鎖住,直到交換完成。
在第 4 步和第 5 步,進行交換,并最終,在第 6 步,兩個 goroutine 都將它們的手從通道里拿出來,這模擬了被鎖住的 goroutine 得到釋放。兩個 goroutine 現(xiàn)在都可以去做別的事情了。
如果沒有指定緩沖區(qū)容量,那么該通道就是同步的,因此會阻塞到發(fā)送者準備好發(fā)送和接收者準備好接收。
無緩沖channel: —— 同步通信
使用簡單的 make 調(diào)用創(chuàng)建的通道叫做無緩沖通道,但 make 還可以接受第二個可選參數(shù),一個表示通道容量的整數(shù)。如果容量是 0,make 創(chuàng)建一個無緩沖通道。
無緩沖通道上的發(fā)送操作將被阻塞,直到另一個 goroutine 在對應的通道上執(zhí)行接受操作,這時值傳送完成,兩個 goroutine 都可以繼續(xù)執(zhí)行。相反,如果接受操作先執(zhí)行,接收方 goroutine 將阻塞,直到另一個 goroutine 在同一個通道上發(fā)送一個值。使用無緩沖通道進行的通信導致發(fā)送和接受操作 goroutine 同步化。因此,無緩沖通道也稱為同步通道。當一個值在無緩沖通道上傳遞時,接受值后發(fā)送方 goroutine 才能被喚醒。
緩沖通道上的發(fā)送操作在隊列的尾部插入一個元素,接收操作從隊列的頭部移除一個元素。如果通道滿了,發(fā)送操作會阻塞所在的 goroutine 直到另一個 goroutine 對它進行接收操作來留出可用的空間。反過來,如果通道是空的,執(zhí)行接收操作的 goroutine 阻塞,直到另一個 goroutine 在通道上發(fā)送數(shù)據(jù)。
如果給一個 nil 的 channel 發(fā)送數(shù)據(jù),會造成永遠阻塞。
如果從一個 nil 的 channel 中接收數(shù)據(jù),也會造成永久阻塞。 給一個已經(jīng)關閉的 channel 發(fā)送數(shù)據(jù), 會引起 panic。
從一個已經(jīng)關閉的 channel 接收數(shù)據(jù), 如果緩沖區(qū)中為空,則返回一個 零 值。
本文是對 Gopher 2017 中一個非常好的 Talk?: [Understanding Channel](GopherCon 2017: Kavya Joshi - Understanding Channels) 的學習筆記,希望能夠通過對 channel 的關鍵特性的理解,進一步掌握其用法細節(jié)以及 Golang 語言設計哲學的管窺蠡測。
channel 是可以讓一個 goroutine 發(fā)送特定值到另一個 gouroutine 的通信機制。
原生的 channel 是沒有緩存的(unbuffered channel),可以用于 goroutine 之間實現(xiàn)同步。
關閉后不能再寫入,可以讀取直到 channel 中再沒有數(shù)據(jù),并返回元素類型的零值。
gopl/ch3/netcat3
首先從 channel 是怎么被創(chuàng)建的開始:
在 heap 上分配一個 hchan 類型的對象,并將其初始化,然后返回一個指向這個 hchan 對象的指針。
理解了 channel 的數(shù)據(jù)結(jié)構(gòu)實現(xiàn),現(xiàn)在轉(zhuǎn)到 channel 的兩個最基本方法: sends 和 receivces ,看一下以上的特性是如何體現(xiàn)在 sends 和 receives 中的:
假設發(fā)送方先啟動,執(zhí)行 ch - task0 :
如此為 channel 帶來了 goroutine-safe 的特性。
在這樣的模型里, sender goroutine - channel - receiver goroutine 之間, hchan 是唯一的共享內(nèi)存,而這個唯一的共享內(nèi)存又通過 mutex 來確保 goroutine-safe ,所有在隊列中的內(nèi)容都只是副本。
這便是著名的 golang 并發(fā)原則的體現(xiàn):
發(fā)送方 goroutine 會阻塞,暫停,并在收到 receive 后才恢復。
goroutine 是一種 用戶態(tài)線程 , 由 Go runtime 創(chuàng)建并管理,而不是操作系統(tǒng),比起操作系統(tǒng)線程來說,goroutine更加輕量。
Go runtime scheduler 負責將 goroutine 調(diào)度到操作系統(tǒng)線程上。
runtime scheduler 怎么將 goroutine 調(diào)度到操作系統(tǒng)線程上?
當阻塞發(fā)生時,一次 goroutine 上下文切換的全過程:
然而,被阻塞的 goroutine 怎么恢復過來?
阻塞發(fā)生時,調(diào)用 runtime sheduler 執(zhí)行 gopark 之前,G1 會創(chuàng)建一個 sudog ,并將它存放在 hchan 的 sendq 中。 sudog 中便記錄了即將被阻塞的 goroutine G1 ,以及它要發(fā)送的數(shù)據(jù)元素 task4 等等。
接收方 將通過這個 sudog 來恢復 G1
接收方 G2 接收數(shù)據(jù), 并發(fā)出一個 receivce ,將 G1 置為 runnable :
同樣的, 接收方 G2 會被阻塞,G2 會創(chuàng)建 sudoq ,存放在 recvq ,基本過程和發(fā)送方阻塞一樣。
不同的是,發(fā)送方 G1如何恢復接收方 G2,這是一個非常神奇的實現(xiàn)。
理論上可以將 task 入隊,然后恢復 G2, 但恢復 G2后,G2會做什么呢?
G2會將隊列中的 task 復制出來,放到自己的 memory 中,基于這個思路,G1在這個時候,直接將 task 寫到 G2的 stack memory 中!
這是違反常規(guī)的操作,理論上 goroutine 之間的 stack 是相互獨立的,只有在運行時可以執(zhí)行這樣的操作。
這么做純粹是出于性能優(yōu)化的考慮,原來的步驟是:
優(yōu)化后,相當于減少了 G2 獲取鎖并且執(zhí)行 memcopy 的性能消耗。
channel 設計背后的思想可以理解為 simplicity 和 performance 之間權衡抉擇,具體如下:
queue with a lock prefered to lock-free implementation:
比起完全 lock-free 的實現(xiàn),使用鎖的隊列實現(xiàn)更簡單,容易實現(xiàn)
1、給一個nil channel發(fā)送數(shù)據(jù),造成永遠阻塞
2、從一個nil channel接收數(shù)據(jù),造成永遠阻塞
3、給一個已經(jīng)關閉的channel發(fā)送數(shù)據(jù),引起panic
4、從一個已經(jīng)關閉的channel接收數(shù)據(jù),如果緩沖區(qū)中為空,則返回一個零值
5、無緩沖的channel是同步的,而有緩沖的channel是非同步的
【譯文】 原文地址
channel是Go語言的一個標志性特性,為go協(xié)程之間的數(shù)據(jù)交互提供一種非常強大的方式,而不需要使用鎖機制。
本文將討論channel的兩個重要屬性,一個是控制協(xié)程間數(shù)據(jù)發(fā)送和接收,以及對channel本身控制。
首先討論下關閉的channel特性。一旦channel被關閉之后,就不能再繼續(xù)發(fā)送數(shù)據(jù)給該channel,但是還是可以繼續(xù)接收channel中的數(shù)據(jù)。如下所示:
output:
上述例子顯示即使ch在for循環(huán)之前已經(jīng)關閉,但還是可以正常的讀取緩存中的true值,讀完之后ok就會被賦值為false表示channel已經(jīng)關閉,而且value值為對應channel類型bool的默認零值false。只要不停地從關閉的channel接收,就會無限的返回默認值和false。可以將for循環(huán)次數(shù)改大點試試即可驗證。
通過以上例子可以發(fā)現(xiàn),關閉的channel可以繼續(xù)接收讀取操作,這種特征是有用的。在使用range讀取帶緩存的channel時就會用到,一旦channel關閉,讀取完緩存中數(shù)據(jù)就會停止接收數(shù)據(jù)退出。
將前面的例子改為如下:
output:
上面的例子就沒有false打出來了。正好是寫入channel里面的兩個值。
channel與select結(jié)合更能發(fā)揮出其作用,讓我們看一個例子:
上面的例子,因為finish在主協(xié)程中發(fā)送之后,馬上就會在select中接收,并執(zhí)行done.Done()。主協(xié)程wait馬上會退出整個程序就結(jié)束。但是這里面存在一個問題,如果在select中沒有添加finish case的話,主協(xié)程就永遠發(fā)送不了數(shù)據(jù)到finish這個channel,因為其不帶緩存。這里就可以通過將finish改成帶緩存的channel,或者可以讓select中的finish不會阻塞。
但是出現(xiàn)多個協(xié)程都在接收finish通道中的數(shù)據(jù)的話,就需要發(fā)送對應協(xié)程數(shù)量的值到channel中才能解決上面的問題。但是具體有多少個協(xié)程這往往是不好確定的,因為有些協(xié)程可能是程序其他部分創(chuàng)建的。一個比較好的選擇就是通過使用關閉通道的方法來實現(xiàn)各協(xié)程能正常接收并結(jié)束。
如下所示:
output:
上面的例子就是使用了關閉的channel可以無限地接收到反饋數(shù)據(jù)。這樣每個協(xié)程都能從finish通道中讀到關閉信息并執(zhí)行done.Done()使得主協(xié)程wait能退出。并且不需要關注多少個協(xié)程數(shù),就能正確的讓所有協(xié)程讀到finish通道信息。
channel的這個特性,可以讓程序員無需關注后臺具體執(zhí)行協(xié)程個數(shù),確保每個協(xié)程都能接收到通道關閉信息,而無需擔心死鎖問題。
通過上面的例子我們也發(fā)現(xiàn)每個協(xié)程并不需要從通道中讀取對應類型的數(shù)據(jù),只需讓接收操作能執(zhí)行就行,讓select不被阻塞。所以可以使用空結(jié)構(gòu)體類型,我們可以改成如下:
這里我們只關注通道是否關閉這個信號,而不需要關注通道里面的數(shù)據(jù),所以可使用空結(jié)構(gòu)體類型通道。
第二個要討論的是nil通道:如果定義了一個channel變量沒有被初始化,或者被賦值為nil,那么該chennel總是處于阻塞狀態(tài)。如下所示:
執(zhí)行結(jié)果為:
因為channel為nil無法發(fā)送數(shù)據(jù),當然也不能接收數(shù)據(jù):
這個似乎看起來不是很重要,但是如果你想使用關閉channel來等待多個channel關閉的話,這個特性就有用處了。先看下面的例子:
WaitMany()函數(shù)看起來好像是一個等待通道a和b關閉的好方法,但是存在一個問題。假設a通道先關閉,case -a就會變成非阻塞。因為bclosed還是false,程序就會進入到一個死循環(huán)當中,導致b通道永遠無法確認關閉。
一個安全的方法就是使用nil通道總是阻塞的特點,如下所示:
上面的例子我們在WaitMany函數(shù)當中,當a或者b關閉時,case可執(zhí)行了將對應的通道賦值為nil,讓其阻塞這樣就可以等待另一個通道關閉。當nil通道是select語句的一部分時,它會被有效地忽略,因此nil通道a會從select中刪除它,只留下b,直到它被關閉,退出循環(huán)。
總之,closed和nil通道的簡單屬性對寫出優(yōu)質(zhì)的go程序是很有用的,可以用來創(chuàng)建高并發(fā)程序。
Hello,大家好,又見面了!上一遍我們將 channel 相關基礎以及使用場景。這一篇,還需要再次進階理解channel 阻塞問題。以下創(chuàng)建一個chan類型為int,cap 為3。
channel 內(nèi)部其實是一個環(huán)形buf數(shù)據(jù)結(jié)構(gòu) ,是一種滑動窗口機制,當make完后,就分配在 Heap 上。
上面,向 chan 發(fā)送一條“hello”數(shù)據(jù):
如果 G1 發(fā)送數(shù)據(jù)超過指定cap時,會出現(xiàn)什么情況?
看下面實例:
以上會出現(xiàn)什么,chan 緩沖區(qū)允許大小為1,如果再往chan仍數(shù)據(jù),滿了就會被阻塞,那么是如何實現(xiàn)阻塞的呢?當 chan 滿時,會進入 gopark,此時 G1 進入一個 waiting 狀態(tài),然后會創(chuàng)建一個 sudog 對象,其實就sendq隊列,把 200放進去。等 buf 不滿的時候,再喚醒放入buf里面。
通過如下源碼,你會更加清晰:
上面,從 chan 獲取數(shù)據(jù):
Go 語言核心思想:“Do not communicate by sharing memory; instead, share memory by communicating.” 你可以看看這本書名叫:Effective Go
如果接收者,接收一個空對象,也會發(fā)生什么情況?
代碼示例 :
也會報錯如下:
上面,從 chan 取出數(shù)據(jù),可是沒有數(shù)據(jù)了。此時,它會把 接收者 G2 阻塞掉,也是和G1發(fā)送者一樣,也會執(zhí)行 gopark 將狀態(tài)改為 waiting,不一樣的點就是。
正常情況下,接收者G2作為取出數(shù)據(jù)是去 buf 讀取數(shù)據(jù)的,但現(xiàn)在,buf 為空了,此時,接收者G2會將sudog導出來,因為現(xiàn)在G2已經(jīng)被阻塞了嘛,會把G2給G,然后將 t := -ch 中變量 t 是在棧上的地址,放進去 elem ,也就是說,只存它的地址指針在sudog里面。
最后, ch - 200 當G1往 chan 添加200這個數(shù)據(jù),正常情況是將數(shù)據(jù)添加到buf里面,然后喚醒 G2 是吧,而現(xiàn)在是將 G1 的添加200數(shù)據(jù)直接干到剛才G2阻塞的t這里變量里面。
你會認為,這樣真的可以嗎?想一想,G2 本來就是已經(jīng)阻塞了,然后我們直接這么干肯定沒有什么毛病,而且效率提高了,不需要再次放入buf再取出,這個過程也是需要時間。不然,不得往chan添加數(shù)據(jù)需要加鎖、拷貝、解鎖一序列操作,那肯定就慢了,我想Go語言是為了高效及內(nèi)存使用率的考慮這樣設計的。(注意,一般都是在runtime里面完成,不然會出現(xiàn)象安全問題。)
總結(jié) :
chan 類型的特點:chan 如果為空,receiver 接收數(shù)據(jù)的時候就會阻塞等待,直到 chan 被關閉或者有新的數(shù)據(jù)到來。有這種個機制,就可以實現(xiàn) wait/notify 的設計模式。
相關面試題: