Go語言 通道 - Go特色的并發(fā)同步方式

2023-02-16 17:38 更新

通道是Go中的一種一等公民類型。它是Go的招牌特性之一。 和另一個(gè)招牌特性協(xié)程一起,這兩個(gè)招牌特性使得使用Go進(jìn)行并發(fā)編程(concurrent programming)變得十分方便和有趣,并且大大降低了并發(fā)編程的難度。

通道的主要作用是用來實(shí)現(xiàn)并發(fā)同步。 本篇文章將列出所有的和通道相關(guān)的概念、語法和規(guī)則。為了更好地理解通道,本文也對(duì)通道的可能的內(nèi)部實(shí)現(xiàn)略加介紹。

本篇文章中的信息量對(duì)于Go初學(xué)者來說可能有些密集。本文的某些段落可能需要反復(fù)閱讀幾遍才能有效吸收、消化和理解。

通道(channel)介紹

Go語言設(shè)計(jì)團(tuán)隊(duì)的首任負(fù)責(zé)人Rob Pike對(duì)并發(fā)編程的一個(gè)建議是不要讓計(jì)算通過共享內(nèi)存來通訊,而應(yīng)該讓它們通過通訊來共享內(nèi)存。 通道機(jī)制就是這種哲學(xué)的一個(gè)設(shè)計(jì)結(jié)果。(在Go編程中,我們可以認(rèn)為一個(gè)計(jì)算就是一個(gè)協(xié)程。)

通過共享內(nèi)存來通訊和通過通訊來共享內(nèi)存是并發(fā)編程中的兩種編程風(fēng)格。 當(dāng)通過共享內(nèi)存來通訊的時(shí)候,我們需要一些傳統(tǒng)的并發(fā)同步技術(shù)(比如互斥鎖)來避免數(shù)據(jù)競爭。

Go提供了一種獨(dú)特的并發(fā)同步技術(shù)來實(shí)現(xiàn)通過通訊來共享內(nèi)存。此技術(shù)即為通道。 我們可以把一個(gè)通道看作是在一個(gè)程序內(nèi)部的一個(gè)先進(jìn)先出(FIFO:first in first out)數(shù)據(jù)隊(duì)列。 一些協(xié)程可以向此通道發(fā)送數(shù)據(jù),另外一些協(xié)程可以從此通道接收數(shù)據(jù)。

隨著一個(gè)數(shù)據(jù)值的傳遞(發(fā)送和接收),一些數(shù)據(jù)值的所有權(quán)從一個(gè)協(xié)程轉(zhuǎn)移到了另一個(gè)協(xié)程。 當(dāng)一個(gè)協(xié)程發(fā)送一個(gè)值到一個(gè)通道,我們可以認(rèn)為此協(xié)程釋放了(通過此發(fā)送值可以訪問到的)一些值的所有權(quán)。 當(dāng)一個(gè)協(xié)程從一個(gè)通道接收到一個(gè)值,我們可以認(rèn)為此協(xié)程獲取了(通過此接受值可以訪問到的)一些值的所有權(quán)。

當(dāng)然,在通過通道傳遞數(shù)據(jù)的時(shí)候,也可能沒有任何所有權(quán)發(fā)生轉(zhuǎn)移。

所有權(quán)發(fā)生轉(zhuǎn)移的值常常被傳遞的值所引用著,但有時(shí)候也并非如此。 在Go中,數(shù)據(jù)所有權(quán)的轉(zhuǎn)移并非體現(xiàn)在語法上,而是體現(xiàn)在邏輯上。 Go通道可以幫助程序員輕松地避免數(shù)據(jù)競爭,但不會(huì)防止程序員因?yàn)榉稿e(cuò)而寫出錯(cuò)誤的并發(fā)代碼的情況發(fā)生。

盡管Go也支持幾種傳統(tǒng)的數(shù)據(jù)同步技術(shù),但是只有通道為一等公民。 通道是Go中的一種類型,所以我們可以無需引進(jìn)任何代碼包就可以使用通道。 幾種傳統(tǒng)的數(shù)據(jù)同步技術(shù)提供在syncsync/atomic標(biāo)準(zhǔn)庫包中。

實(shí)事求是地說,每種并發(fā)同步技術(shù)都有它們各自的最佳應(yīng)用場景,但是通道的應(yīng)用范圍更廣。 使用通道來做同步常??梢允沟么a看上去更整潔和易于理解。

通道的一個(gè)問題是通道的編程體驗(yàn)常常很有趣以至于程序員們經(jīng)常在并非是通道的最佳應(yīng)用場景中仍堅(jiān)持使用通道。

通道類型和值

和數(shù)組、切片以及映射類型一樣,每個(gè)通道類型也有一個(gè)元素類型。 一個(gè)通道只能傳送它的(通道類型的)元素類型的值。

通道可以是雙向的,也可以是單向的。

  • 字面形式chan T表示一個(gè)元素類型為T的雙向通道類型。 編譯器允許從此類型的值中接收和向此類型的值中發(fā)送數(shù)據(jù)。
  • 字面形式chan<- T表示一個(gè)元素類型為T的單向發(fā)送通道類型。 編譯器不允許從此類型的值中接收數(shù)據(jù)。
  • 字面形式<-chan T表示一個(gè)元素類型為T的單向接收通道類型。 編譯器不允許向此類型的值中發(fā)送數(shù)據(jù)。

雙向通道chan T的值可以被隱式轉(zhuǎn)換為單向通道類型chan<- T<-chan T,但反之不行(即使顯式也不行)。 類型chan<- T<-chan T的值也不能相互轉(zhuǎn)換。

每個(gè)通道值有一個(gè)容量屬性。此屬性的意義將在下一節(jié)中得到解釋。 一個(gè)容量為0的通道值稱為一個(gè)非緩沖通道(unbuffered channel),一個(gè)容量不為0的通道值稱為一個(gè)緩沖通道(buffered channel)。

通道類型的零值也使用預(yù)聲明的nil來表示。 一個(gè)非零通道值必須通過內(nèi)置的make函數(shù)來創(chuàng)建。 比如make(chan int, 10)將創(chuàng)建一個(gè)元素類型為int的通道值。 第二個(gè)參數(shù)指定了欲創(chuàng)建的通道的容量。此第二個(gè)實(shí)參是可選的,它的默認(rèn)值為0。

通道值的比較

所有通道類型均為可比較類型。

值部一文,我們了解到一個(gè)通道值可能含有底層部分。 當(dāng)一個(gè)通道值被賦給另一個(gè)通道值后,這兩個(gè)通道值將共享相同的底層部分。 換句話說,這兩個(gè)通道引用著同一個(gè)底層的內(nèi)部通道對(duì)象。 比較這兩個(gè)通道的結(jié)果為true。

通道操作

Go中有五種通道相關(guān)的操作。假設(shè)一個(gè)通道(值)為ch,下面列出了這五種操作的語法或者函數(shù)調(diào)用。

  1. 調(diào)用內(nèi)置函數(shù)close來關(guān)閉一個(gè)通道:
    close(ch)
    

    傳給close函數(shù)調(diào)用的實(shí)參必須為一個(gè)通道值,并且此通道值不能為單向接收的。

  2. 使用下面的語法向通道ch發(fā)送一個(gè)值v
    ch <- v
    

    v必須能夠賦值給通道ch的元素類型。 ch不能為單向接收通道。 <-稱為數(shù)據(jù)發(fā)送操作符。

  3. 使用下面的語法從通道ch接收一個(gè)值:
    <-ch
    
    如果一個(gè)通道操作不永久阻塞,它總會(huì)返回至少一個(gè)值,此值的類型為通道ch的元素類型。 ch不能為單向發(fā)送通道。 <-稱為數(shù)據(jù)接收操作符,是的它和數(shù)據(jù)發(fā)送操作符的表示形式是一樣的。 在大多數(shù)場合下,一個(gè)數(shù)據(jù)接收操作可以被認(rèn)為是一個(gè)單值表達(dá)式。 但是,當(dāng)一個(gè)數(shù)據(jù)接收操作被用做一個(gè)賦值語句中的唯一的源值的時(shí)候,它可以返回第二個(gè)可選的類型不確定的布爾值返回值從而成為一個(gè)多值表達(dá)式。 此類型不確定的布爾值表示第一個(gè)接收到的值是否是在通道被關(guān)閉前發(fā)送的。 (從后面的章節(jié),我們將得知我們可以從一個(gè)已關(guān)閉的通道中接收到無窮個(gè)值。) 數(shù)據(jù)接收操作在賦值中被用做源值的例子:
    v = <-ch
    v, sentBeforeClosed = <-ch
    
  4. 查詢一個(gè)通道的容量:
    cap(ch)
    

    其中cap是一個(gè)已經(jīng)在容器類型一文中介紹過的內(nèi)置函數(shù)。 cap的返回值的類型為內(nèi)置類型int。

  5. 查詢一個(gè)通道的長度:
    len(ch)
    

    其中len是一個(gè)已經(jīng)在容器類型一文中介紹過的內(nèi)置函數(shù)。 len的返回值的類型也為內(nèi)置類型int。 一個(gè)通道的長度是指當(dāng)前有多少個(gè)已被發(fā)送到此通道但還未被接收出去的元素值。

Go中大多數(shù)的基本操作都是未同步的。換句話說,它們都不是并發(fā)安全的。 這些操作包括賦值、傳參、和各種容器值操作等。 但是,上面列出的五種通道相關(guān)的操作都已經(jīng)同步過了,因此它們可以在并發(fā)協(xié)程中安全運(yùn)行而無需其它同步操作。

注意:通道的賦值和其它類型值的賦值一樣,是未同步的。 同樣,將剛從一個(gè)通道接收出來的值賦給另一個(gè)值也是未同步的。

如果被查詢的通道為一個(gè)nil零值通道,則caplen函數(shù)調(diào)用都返回0。 這兩個(gè)操作是如此簡單,所以后面將不再對(duì)它們進(jìn)行詳解。 事實(shí)上,這兩個(gè)操作在實(shí)踐中很少使用。

通道的發(fā)送、接收和關(guān)閉操作將在下一節(jié)得到詳細(xì)解釋。

通道操作詳解

為了讓解釋簡單清楚,在本文后續(xù)部分,通道將被歸為三類:

  1. 零值(nil)通道;
  2. 非零值但已關(guān)閉的通道;
  3. 非零值并且尚未關(guān)閉的通道。

下表簡單地描述了三種通道操作施加到三類通道的結(jié)果。

操作 一個(gè)零值nil通道 一個(gè)非零值但已關(guān)閉的通道 一個(gè)非零值且尚未關(guān)閉的通道
關(guān)閉 產(chǎn)生恐慌 產(chǎn)生恐慌 成功關(guān)閉(C)
發(fā)送數(shù)據(jù) 永久阻塞 產(chǎn)生恐慌 阻塞或者成功發(fā)送(B)
接收數(shù)據(jù) 永久阻塞 永不阻塞(D) 阻塞或者成功接收(A)

對(duì)于上表中的五種未打上標(biāo)的情形,規(guī)則很簡單:

  • 關(guān)閉一個(gè)nil通道或者一個(gè)已經(jīng)關(guān)閉的通道將產(chǎn)生一個(gè)恐慌。
  • 向一個(gè)已關(guān)閉的通道發(fā)送數(shù)據(jù)也將導(dǎo)致一個(gè)恐慌。
  • 向一個(gè)nil通道發(fā)送數(shù)據(jù)或者從一個(gè)nil通道接收數(shù)據(jù)將使當(dāng)前協(xié)程永久阻塞。

下面將詳細(xì)解釋其它四種被打了上標(biāo)(A/B/C/D)的情形。

為了更好地理解通道和為了后續(xù)講解方便,先了解一下通道類型的大致內(nèi)部實(shí)現(xiàn)是很有幫助的。

我們可以認(rèn)為一個(gè)通道內(nèi)部維護(hù)了三個(gè)隊(duì)列(均可被視為先進(jìn)先出隊(duì)列):

  1. 接收數(shù)據(jù)協(xié)程隊(duì)列(可以看做是先進(jìn)先出隊(duì)列但其實(shí)并不完全是,見下面解釋)。此隊(duì)列是一個(gè)沒有長度限制的鏈表。 此隊(duì)列中的協(xié)程均處于阻塞狀態(tài),它們正等待著從此通道接收數(shù)據(jù)。
  2. 發(fā)送數(shù)據(jù)協(xié)程隊(duì)列(可以看做是先進(jìn)先出隊(duì)列但其實(shí)并不完全是,見下面解釋)。此隊(duì)列也是一個(gè)沒有長度限制的鏈表。 此隊(duì)列中的協(xié)程亦均處于阻塞狀態(tài),它們正等待著向此通道發(fā)送數(shù)據(jù)。 此隊(duì)列中的每個(gè)協(xié)程將要發(fā)送的值(或者此值的指針,取決于具體編譯器實(shí)現(xiàn))和此協(xié)程一起存儲(chǔ)在此隊(duì)列中。
  3. 數(shù)據(jù)緩沖隊(duì)列。這是一個(gè)循環(huán)隊(duì)列(絕對(duì)先進(jìn)先出),它的長度為此通道的容量。此隊(duì)列中存放的值的類型都為此通道的元素類型。 如果此隊(duì)列中當(dāng)前存放的值的個(gè)數(shù)已經(jīng)達(dá)到此通道的容量,則我們說此通道已經(jīng)處于滿槽狀態(tài)。 如果此隊(duì)列中當(dāng)前存放的值的個(gè)數(shù)為零,則我們說此通道處于空槽狀態(tài)。 對(duì)于一個(gè)非緩沖通道(容量為零),它總是同時(shí)處于滿槽狀態(tài)和空槽狀態(tài)。

每個(gè)通道內(nèi)部維護(hù)著一個(gè)互斥鎖用來在各種通道操作中防止數(shù)據(jù)競爭。

通道操作情形A: 當(dāng)一個(gè)協(xié)程R嘗試從一個(gè)非零且尚未關(guān)閉的通道接收數(shù)據(jù)的時(shí)候,此協(xié)程R將首先嘗試獲取此通道的鎖,成功之后將執(zhí)行下列步驟,直到其中一個(gè)步驟的條件得到滿足。

  1. 如果此通道的緩沖隊(duì)列不為空(這種情況下,接收數(shù)據(jù)協(xié)程隊(duì)列必為空),此協(xié)程R將從緩沖隊(duì)列取出(接收)一個(gè)值。 如果發(fā)送數(shù)據(jù)協(xié)程隊(duì)列不為空,一個(gè)發(fā)送協(xié)程將從此隊(duì)列中彈出,此協(xié)程欲發(fā)送的值將被推入緩沖隊(duì)列。此發(fā)送協(xié)程將恢復(fù)至運(yùn)行狀態(tài)。 接收數(shù)據(jù)協(xié)程R繼續(xù)運(yùn)行,不會(huì)阻塞。對(duì)于這種情況,此數(shù)據(jù)接收操作為一個(gè)非阻塞操作。
  2. 否則(即此通道的緩沖隊(duì)列為空),如果發(fā)送數(shù)據(jù)協(xié)程隊(duì)列不為空(這種情況下,此通道必為一個(gè)非緩沖通道), 一個(gè)發(fā)送數(shù)據(jù)協(xié)程將從此隊(duì)列中彈出,此協(xié)程欲發(fā)送的值將被接收數(shù)據(jù)協(xié)程R接收。此發(fā)送協(xié)程將恢復(fù)至運(yùn)行狀態(tài)。 接收數(shù)據(jù)協(xié)程R繼續(xù)運(yùn)行,不會(huì)阻塞。對(duì)于這種情況,此數(shù)據(jù)接收操作為一個(gè)非阻塞操作。
  3. 對(duì)于剩下的情況(即此通道的緩沖隊(duì)列和發(fā)送數(shù)據(jù)協(xié)程隊(duì)列均為空),此接收數(shù)據(jù)協(xié)程R將被推入接收數(shù)據(jù)協(xié)程隊(duì)列,并進(jìn)入阻塞狀態(tài)。 它以后可能會(huì)被另一個(gè)發(fā)送數(shù)據(jù)協(xié)程喚醒而恢復(fù)運(yùn)行。 對(duì)于這種情況,此數(shù)據(jù)接收操作為一個(gè)阻塞操作。

通道操作情形B: 當(dāng)一個(gè)協(xié)程S嘗試向一個(gè)非零且尚未關(guān)閉的通道發(fā)送數(shù)據(jù)的時(shí)候,此協(xié)程S將首先嘗試獲取此通道的鎖,成功之后將執(zhí)行下列步驟,直到其中一個(gè)步驟的條件得到滿足。

  1. 如果此通道的接收數(shù)據(jù)協(xié)程隊(duì)列不為空(這種情況下,緩沖隊(duì)列必為空), 一個(gè)接收數(shù)據(jù)協(xié)程將從此隊(duì)列中彈出,此協(xié)程將接收到發(fā)送協(xié)程S發(fā)送的值。此接收協(xié)程將恢復(fù)至運(yùn)行狀態(tài)。 發(fā)送數(shù)據(jù)協(xié)程S繼續(xù)運(yùn)行,不會(huì)阻塞。對(duì)于這種情況,此數(shù)據(jù)發(fā)送操作為一個(gè)非阻塞操作。
  2. 否則(接收數(shù)據(jù)協(xié)程隊(duì)列為空),如果緩沖隊(duì)列未滿(這種情況下,發(fā)送數(shù)據(jù)協(xié)程隊(duì)列必為空), 發(fā)送協(xié)程S欲發(fā)送的值將被推入緩沖隊(duì)列,發(fā)送數(shù)據(jù)協(xié)程S繼續(xù)運(yùn)行,不會(huì)阻塞。 對(duì)于這種情況,此數(shù)據(jù)發(fā)送操作為一個(gè)非阻塞操作。
  3. 對(duì)于剩下的情況(接收數(shù)據(jù)協(xié)程隊(duì)列為空,并且緩沖隊(duì)列已滿),此發(fā)送協(xié)程S將被推入發(fā)送數(shù)據(jù)協(xié)程隊(duì)列,并進(jìn)入阻塞狀態(tài)。 它以后可能會(huì)被另一個(gè)接收數(shù)據(jù)協(xié)程喚醒而恢復(fù)運(yùn)行。 對(duì)于這種情況,此數(shù)據(jù)發(fā)送操作為一個(gè)阻塞操作。

上面已經(jīng)提到過,一旦一個(gè)非零通道被關(guān)閉,繼續(xù)向此通道發(fā)送數(shù)據(jù)將產(chǎn)生一個(gè)恐慌。 注意,向關(guān)閉的通道發(fā)送數(shù)據(jù)屬于一個(gè)非阻塞操作。

通道操作情形C: 當(dāng)一個(gè)協(xié)程成功獲取到一個(gè)非零且尚未關(guān)閉的通道的鎖并且準(zhǔn)備關(guān)閉此通道時(shí),下面兩步將依次執(zhí)行:

  1. 如果此通道的接收數(shù)據(jù)協(xié)程隊(duì)列不為空(這種情況下,緩沖隊(duì)列必為空),此隊(duì)列中的所有協(xié)程將被依個(gè)彈出,并且每個(gè)協(xié)程將接收到此通道的元素類型的一個(gè)零值,然后恢復(fù)至運(yùn)行狀態(tài)。
  2. 如果此通道的發(fā)送數(shù)據(jù)協(xié)程隊(duì)列不為空,此隊(duì)列中的所有協(xié)程將被依個(gè)彈出,并且每個(gè)協(xié)程中都將產(chǎn)生一個(gè)恐慌(因?yàn)橄蛞殃P(guān)閉的通道發(fā)送數(shù)據(jù))。 這就是我們?cè)谏厦嬲f并發(fā)地關(guān)閉一個(gè)通道和向此通道發(fā)送數(shù)據(jù)這種情形屬于不良設(shè)計(jì)的原因。 事實(shí)上,在數(shù)據(jù)競爭偵測(cè)編譯選項(xiàng)(-race)打開時(shí),Go官方標(biāo)準(zhǔn)運(yùn)行時(shí)將很可能會(huì)對(duì)并發(fā)地關(guān)閉一個(gè)通道和向此通道發(fā)送數(shù)據(jù)這種情形報(bào)告成數(shù)據(jù)競爭。

注意:當(dāng)一個(gè)緩沖隊(duì)列不為空的通道被關(guān)閉之后,它的緩沖隊(duì)列不會(huì)被清空,其中的數(shù)據(jù)仍然可以被后續(xù)的數(shù)據(jù)接收操作所接收到。詳見下面的對(duì)情形D的解釋。

通道操作情形D: 一個(gè)非零通道被關(guān)閉之后,此通道上的后續(xù)數(shù)據(jù)接收操作將永不會(huì)阻塞。 此通道的緩沖隊(duì)列中存儲(chǔ)數(shù)據(jù)仍然可以被接收出來。 伴隨著這些接收出來的緩沖數(shù)據(jù)的第二個(gè)可選返回(類型不確定布爾)值仍然是true。 一旦此緩沖隊(duì)列變?yōu)榭?,后續(xù)的數(shù)據(jù)接收操作將永不阻塞并且總會(huì)返回此通道的元素類型的零值和值為false的第二個(gè)可選返回結(jié)果。 上面已經(jīng)提到了,一個(gè)接收操作的第二個(gè)可選返回(類型不確定布爾)結(jié)果表示一個(gè)接收到的值是否是在此通道被關(guān)閉之前發(fā)送的。 如果此返回值為false,則第一個(gè)返回值必然是一個(gè)此通道的元素類型的零值。

知道哪些通道操作是阻塞的和哪些是非阻塞的對(duì)正確理解后面將要介紹的select流程控制機(jī)制非常重要。

如果一個(gè)協(xié)程被從一個(gè)通道的某個(gè)隊(duì)列中(不論發(fā)送數(shù)據(jù)協(xié)程隊(duì)列還是接收數(shù)據(jù)協(xié)程隊(duì)列)彈出,并且此協(xié)程是在一個(gè)select控制流程中推入到此隊(duì)列的,那么此協(xié)程將在下面將要講解的select控制流程的執(zhí)行步驟中的第9步中恢復(fù)至運(yùn)行狀態(tài),并且同時(shí)它會(huì)被從相應(yīng)的select控制流程中的相關(guān)的若干通道的協(xié)程隊(duì)列中移除掉。

根據(jù)上面的解釋,我們可以得出如下的關(guān)于一個(gè)通道的內(nèi)部的三個(gè)隊(duì)列的各種事實(shí):

  • 如果一個(gè)通道已經(jīng)關(guān)閉了,則它的發(fā)送數(shù)據(jù)協(xié)程隊(duì)列和接收數(shù)據(jù)協(xié)程隊(duì)列肯定都為空,但是它的緩沖隊(duì)列可能不為空。
  • 在任何時(shí)刻,如果緩沖隊(duì)列不為空,則接收數(shù)據(jù)協(xié)程隊(duì)列必為空。
  • 在任何時(shí)刻,如果緩沖隊(duì)列未滿,則發(fā)送數(shù)據(jù)協(xié)程隊(duì)列必為空。
  • 如果一個(gè)通道是緩沖的,則在任何時(shí)刻,它的發(fā)送數(shù)據(jù)協(xié)程隊(duì)列和接收數(shù)據(jù)協(xié)程隊(duì)列之一必為空。
  • 如果一個(gè)通道是非緩沖的,則在任何時(shí)刻,一般說來,它的發(fā)送數(shù)據(jù)協(xié)程隊(duì)列和接收數(shù)據(jù)協(xié)程隊(duì)列之一必為空, 但是有一個(gè)例外:一個(gè)協(xié)程可能在一個(gè)select流程控制中同時(shí)被推入到此通道的發(fā)送數(shù)據(jù)協(xié)程隊(duì)列和接收數(shù)據(jù)協(xié)程隊(duì)列中。

一些通道的使用例子

來看一些通道的使用例子來加深一下對(duì)上一節(jié)中的解釋的理解。

一個(gè)簡單的通過一個(gè)非緩沖通道實(shí)現(xiàn)的請(qǐng)求/響應(yīng)的例子:

package main

import (
	"fmt"
	"time"
)

func main() {
	c := make(chan int) // 一個(gè)非緩沖通道
	go func(ch chan<- int, x int) {
		time.Sleep(time.Second)
		// <-ch    // 此操作編譯不通過
		ch <- x*x  // 阻塞在此,直到發(fā)送的值被接收
	}(c, 3)
	done := make(chan struct{})
	go func(ch <-chan int) {
		n := <-ch      // 阻塞在此,直到有值發(fā)送到c
		fmt.Println(n) // 9
		// ch <- 123   // 此操作編譯不通過
		time.Sleep(time.Second)
		done <- struct{}{}
	}(c)
	<-done // 阻塞在此,直到有值發(fā)送到done
	fmt.Println("bye")
}

輸出結(jié)果:

9
bye

下面的例子使用了一個(gè)緩沖通道。此例子程序并非是一個(gè)并發(fā)程序,它只是為了展示緩沖通道的使用。

package main

import "fmt"

func main() {
	c := make(chan int, 2) // 一個(gè)容量為2的緩沖通道
	c <- 3
	c <- 5
	close(c)
	fmt.Println(len(c), cap(c)) // 2 2
	x, ok := <-c
	fmt.Println(x, ok) // 3 true
	fmt.Println(len(c), cap(c)) // 1 2
	x, ok = <-c
	fmt.Println(x, ok) // 5 true
	fmt.Println(len(c), cap(c)) // 0 2
	x, ok = <-c
	fmt.Println(x, ok) // 0 false
	x, ok = <-c
	fmt.Println(x, ok) // 0 false
	fmt.Println(len(c), cap(c)) // 0 2
	close(c) // 此行將產(chǎn)生一個(gè)恐慌
	c <- 7   // 如果上一行不存在,此行也將產(chǎn)生一個(gè)恐慌。
}

一場永不休場的足球比賽:

package main

import (
	"fmt"
	"time"
)

func main() {
	var ball = make(chan string)
	kickBall := func(playerName string) {
		for {
			fmt.Print(<-ball, "傳球", "\n")
			time.Sleep(time.Second)
			ball <- playerName
		}
	}
	go kickBall("張三")
	go kickBall("李四")
	go kickBall("王二麻子")
	go kickBall("劉大")
	ball <- "裁判"   // 開球
	var c chan bool // 一個(gè)零值nil通道
	<-c             // 永久阻塞在此
}

請(qǐng)閱讀通道用例大全來查看更多通道的使用例子。

通道的元素值的傳遞都是復(fù)制過程

在一個(gè)值被從一個(gè)協(xié)程傳遞到另一個(gè)協(xié)程的過程中,此值將被復(fù)制至少一次。 如果此傳遞值曾經(jīng)在某個(gè)通道的緩沖隊(duì)列中停留過,則它在此傳遞過程中將被復(fù)制兩次。 一次復(fù)制發(fā)生在從發(fā)送協(xié)程向緩沖隊(duì)列推入此值的時(shí)候,另一個(gè)復(fù)制發(fā)生在接收協(xié)程從緩沖隊(duì)列取出此值的時(shí)候。 和賦值以及函數(shù)調(diào)用傳參一樣,當(dāng)一個(gè)值被傳遞時(shí),只有它的直接部分被復(fù)制。

對(duì)于官方標(biāo)準(zhǔn)編譯器,最大支持的通道的元素類型的尺寸為65535。 但是,一般說來,為了在數(shù)據(jù)傳遞過程中避免過大的復(fù)制成本,我們不應(yīng)該使用尺寸很大的通道元素類型。 如果欲傳送的值的尺寸較大,應(yīng)該改用指針類型做為通道的元素類型。

關(guān)于通道和協(xié)程的垃圾回收

注意,一個(gè)通道被其發(fā)送數(shù)據(jù)協(xié)程隊(duì)列和接收數(shù)據(jù)協(xié)程隊(duì)列中的所有協(xié)程引用著。因此,如果一個(gè)通道的這兩個(gè)隊(duì)列只要有一個(gè)不為空,則此通道肯定不會(huì)被垃圾回收。 另一方面,如果一個(gè)協(xié)程處于一個(gè)通道的某個(gè)協(xié)程隊(duì)列之中,則此協(xié)程也肯定不會(huì)被垃圾回收,即使此通道僅被此協(xié)程所引用。 事實(shí)上,一個(gè)協(xié)程只有在退出后才能被垃圾回收。

數(shù)據(jù)接收和發(fā)送操作都屬于簡單語句

數(shù)據(jù)接收和發(fā)送操作都屬于簡單語句。 另外一個(gè)數(shù)據(jù)接收操作總是可以被用做一個(gè)單值表達(dá)式。 簡單語句和表達(dá)式可以被用在一些控制流程的某些部分。

在下面這個(gè)例子中,數(shù)據(jù)接收和發(fā)送操作被用在兩個(gè)for循環(huán)的初始化和步尾語句。

package main

import (
	"fmt"
	"time"
)

func main() {
	fibonacci := func() chan uint64 {
		c := make(chan uint64)
		go func() {
			var x, y uint64 = 0, 1
			for ; y < (1 << 63); c <- y { // 步尾語句
				x, y = y, x+y
			}
			close(c)
		}()
		return c
	}
	c := fibonacci()
	for x, ok := <-c; ok; x, ok = <-c { // 初始化和步尾語句
		time.Sleep(time.Second)
		fmt.Println(x)
	}
}

for-range應(yīng)用于通道

for-range循環(huán)控制流程也適用于通道。 此循環(huán)將不斷地嘗試從一個(gè)通道接收數(shù)據(jù),直到此通道關(guān)閉并且它的緩沖隊(duì)列為空為止。 和應(yīng)用于數(shù)組/切片/映射的for-range語法不同,應(yīng)用于通道的for-range語法中最多只能出現(xiàn)一個(gè)循環(huán)變量,此循環(huán)變量用來存儲(chǔ)接收到的值。

for v := range aChannel {
	// 使用v
}

等價(jià)于

for {
	v, ok = <-aChannel
	if !ok {
		break
	}
	// 使用v
}

當(dāng)然,這里的通道aChannel一定不能為一個(gè)單向發(fā)送通道。 如果它是一個(gè)nil零值,則此for-range循環(huán)將使當(dāng)前協(xié)程永久阻塞。

上一節(jié)中的例子中的最后一個(gè)for循環(huán)可以改寫為下面這樣:

	for x := range c {
		time.Sleep(time.Second)
		fmt.Println(x)
	}

select-case分支流程控制代碼塊

Go中有一個(gè)專門為通道設(shè)計(jì)的select-case分支流程控制語法。 此語法和switch-case分支流程控制語法很相似。 比如,select-case流程控制代碼塊中也可以有若干case分支和最多一個(gè)default分支。 但是,這兩種流程控制也有很多不同點(diǎn)。在一個(gè)select-case流程控制中,

  • select關(guān)鍵字和{之間不允許存在任何表達(dá)式和語句。
  • fallthrough語句不能被使用.
  • 每個(gè)case關(guān)鍵字后必須跟隨一個(gè)通道接收數(shù)據(jù)操作或者一個(gè)通道發(fā)送數(shù)據(jù)操作。 通道接收數(shù)據(jù)操作可以做為源值出現(xiàn)在一條簡單賦值語句中。 以后,一個(gè)case關(guān)鍵字后跟隨的通道操作將被稱為一個(gè)case操作。
  • 所有的非阻塞case操作中將有一個(gè)被隨機(jī)選擇執(zhí)行(而不是按照從上到下的順序),然后執(zhí)行此操作對(duì)應(yīng)的case分支代碼塊。
  • 在所有的case操作均為阻塞的情況下,如果default分支存在,則default分支代碼塊將得到執(zhí)行; 否則,當(dāng)前協(xié)程將被推入所有阻塞操作中相關(guān)的通道的發(fā)送數(shù)據(jù)協(xié)程隊(duì)列或者接收數(shù)據(jù)協(xié)程隊(duì)列中,并進(jìn)入阻塞狀態(tài)。

按照上述規(guī)則,一個(gè)不含任何分支的select-case代碼塊select{}將使當(dāng)前協(xié)程處于永久阻塞狀態(tài)。

在下面這個(gè)例子中,default分支將鐵定得到執(zhí)行,因?yàn)閮蓚€(gè)case分支后的操作均為阻塞的。

package main

import "fmt"

func main() {
	var c chan struct{} // nil
	select {
	case <-c:             // 阻塞操作
	case c <- struct{}{}: // 阻塞操作
	default:
		fmt.Println("Go here.")
	}
}

下面這個(gè)例子中實(shí)現(xiàn)了嘗試發(fā)送(try-send)和嘗試接收(try-receive)。 它們都是用含有一個(gè)case分支和一個(gè)default分支的select-case代碼塊來實(shí)現(xiàn)的。

package main

import "fmt"

func main() {
	c := make(chan string, 2)
	trySend := func(v string) {
		select {
		case c <- v:
		default: // 如果c的緩沖已滿,則執(zhí)行默認(rèn)分支。
		}
	}
	tryReceive := func() string {
		select {
		case v := <-c: return v
		default: return "-" // 如果c的緩沖為空,則執(zhí)行默認(rèn)分支。
		}
	}
	trySend("Hello!") // 發(fā)送成功
	trySend("Hi!")    // 發(fā)送成功
	trySend("Bye!")   // 發(fā)送失敗,但不會(huì)阻塞。
	// 下面這兩行將接收成功。
	fmt.Println(tryReceive()) // Hello!
	fmt.Println(tryReceive()) // Hi!
	// 下面這行將接收失敗。
	fmt.Println(tryReceive()) // -
}

下面這個(gè)程序有50%的幾率會(huì)因?yàn)榭只哦罎ⅰ?此程序中select-case代碼塊中的兩個(gè)case操作均不阻塞,所以隨機(jī)一個(gè)將被執(zhí)行。 如果第一個(gè)case操作(向已關(guān)閉的通道發(fā)送數(shù)據(jù))被執(zhí)行,則一個(gè)恐慌將產(chǎn)生。

package main

func main() {
	c := make(chan struct{})
	close(c)
	select {
	case c <- struct{}{}: // 若此分支被選中,則產(chǎn)生一個(gè)恐慌
	case <-c:
	}
}

select-case流程控制的實(shí)現(xiàn)機(jī)理

select-case流程控制是Go中的一個(gè)重要和獨(dú)特的特性。 下面列出了官方標(biāo)準(zhǔn)運(yùn)行時(shí)中select-case流程控制的實(shí)現(xiàn)步驟。

  1. 將所有case操作中涉及到的通道表達(dá)式和發(fā)送值表達(dá)式按照從上到下,從左到右的順序一一估值。 在賦值語句中做為源值的數(shù)據(jù)接收操作對(duì)應(yīng)的目標(biāo)值在此時(shí)刻不需要被估值。
  2. 將所有分支隨機(jī)排序。default分支總是排在最后。 所有case操作中相關(guān)的通道可能會(huì)有重復(fù)的。
  3. 為了防止在下一步中造成(和其它協(xié)程互相)死鎖,對(duì)所有case操作中相關(guān)的通道進(jìn)行排序。 排序依據(jù)并不重要,官方Go標(biāo)準(zhǔn)編譯器使用通道的地址順序進(jìn)行排序。 排序結(jié)果中前N個(gè)通道不存在重復(fù)的情況。 N為所有case操作中涉及到的不重復(fù)的通道的數(shù)量。 下面,通道鎖順序是針對(duì)此排序結(jié)果中的前N個(gè)通道來說的,通道鎖逆序是指此順序的逆序。
  4. 按照上一步中的生成通道鎖順序獲取所有相關(guān)的通道的鎖。
  5. 按照第2步中生成的分支順序檢查相應(yīng)分支:
    1. 如果這是一個(gè)case分支并且相應(yīng)的通道操作是一個(gè)向關(guān)閉了的通道發(fā)送數(shù)據(jù)操作,則按照通道鎖逆序解鎖所有的通道并在當(dāng)前協(xié)程中產(chǎn)生一個(gè)恐慌。 跳到第12步。
    2. 如果這是一個(gè)case分支并且相應(yīng)的通道操作是非阻塞的,則按照通道鎖逆序解鎖所有的通道并執(zhí)行相應(yīng)的case分支代碼塊。 (此相應(yīng)的通道操作可能會(huì)喚醒另一個(gè)處于阻塞狀態(tài)的協(xié)程。) 跳到第12步。
    3. 如果這是default分支,則按照通道鎖逆序解鎖所有的通道并執(zhí)行此default分支代碼塊。 跳到第12步。
    (到這里,default分支肯定是不存在的,并且所有的case操作均為阻塞的。)
  6. 將當(dāng)前協(xié)程(和對(duì)應(yīng)case分支信息)推入到每個(gè)case操作中對(duì)應(yīng)的通道的發(fā)送數(shù)據(jù)協(xié)程隊(duì)列或接收數(shù)據(jù)協(xié)程隊(duì)列中。 當(dāng)前協(xié)程可能會(huì)被多次推入到同一個(gè)通道的這兩個(gè)隊(duì)列中,因?yàn)槎鄠€(gè)case操作中對(duì)應(yīng)的通道可能為同一個(gè)。
  7. 使當(dāng)前協(xié)程進(jìn)入阻塞狀態(tài)并且按照通道鎖逆序解鎖所有的通道。
  8. ...,當(dāng)前協(xié)程處于阻塞狀態(tài),等待其它協(xié)程通過通道操作喚醒當(dāng)前協(xié)程,...
  9. 當(dāng)前協(xié)程被另一個(gè)協(xié)程中的一個(gè)通道操作喚醒。 此喚醒通道操作可能是一個(gè)通道關(guān)閉操作,也可能是一個(gè)數(shù)據(jù)發(fā)送/接收操作。 如果它是一個(gè)數(shù)據(jù)發(fā)送/接收操作,則(當(dāng)前正被解釋的select-case流程中)肯定有一個(gè)相應(yīng)case操作與之配合傳遞數(shù)據(jù)。 在此配合過程中,當(dāng)前協(xié)程將從相應(yīng)case操作相關(guān)的通道的接收/發(fā)送數(shù)據(jù)協(xié)程隊(duì)列中彈出。
  10. 按照第3步中的生成的通道鎖順序獲取所有相關(guān)的通道的鎖。
  11. 將當(dāng)前協(xié)程從各個(gè)case操作中對(duì)應(yīng)的通道的發(fā)送數(shù)據(jù)協(xié)程隊(duì)列或接收數(shù)據(jù)協(xié)程隊(duì)列中(可能以非彈出的方式)移除。
    1. 如果當(dāng)前協(xié)程是被一個(gè)通道關(guān)閉操作所喚醒,則跳到第5步。
    2. 如果當(dāng)前協(xié)程是被一個(gè)數(shù)據(jù)發(fā)送/接收操作所喚醒,則相應(yīng)的case分支已經(jīng)在第9步中知曉。 按照通道鎖逆序解鎖所有的通道并執(zhí)行此case分支代碼塊。
  12. 完畢。

從此實(shí)現(xiàn)中,我們得知

  • 一個(gè)協(xié)程可能同時(shí)多次處于同一個(gè)通道的發(fā)送數(shù)據(jù)協(xié)程隊(duì)列或接收數(shù)據(jù)協(xié)程隊(duì)列中。
  • 當(dāng)一個(gè)協(xié)程被阻塞在一個(gè)select-case流程控制中并在以后被喚醒時(shí),它可能會(huì)從多個(gè)通道的發(fā)送數(shù)據(jù)協(xié)程隊(duì)列和接收數(shù)據(jù)協(xié)程隊(duì)列中被移除。

更多

我們可以在通道用例大全一文中找到更多通道的使用例子。

盡管通道可以幫助我們輕松地寫出正確的并發(fā)代碼,和其它并發(fā)同步技術(shù)一樣,通道并不會(huì)阻止我們寫出不正確的并發(fā)代碼。

通道并非在任何場合總是最佳的并發(fā)同步方案,請(qǐng)閱讀其它并發(fā)同步技術(shù)原子操作來了解Go中支持的更多的并發(fā)同步技術(shù)。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)