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

2023-02-16 17:38 更新

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

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

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

通道(channel)介紹

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

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

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

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

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

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

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

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

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

通道類型和值

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

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

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

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

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

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

通道值的比較

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

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

通道操作

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

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

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

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

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

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

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

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

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

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

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

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

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

通道操作詳解

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

一些通道的使用例子

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

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

package main

import (
	"fmt"
	"time"
)

func main() {
	c := make(chan int) // 一個非緩沖通道
	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

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

package main

import "fmt"

func main() {
	c := make(chan int, 2) // 一個容量為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)生一個恐慌
	c <- 7   // 如果上一行不存在,此行也將產(chǎn)生一個恐慌。
}

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

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 // 一個零值nil通道
	<-c             // 永久阻塞在此
}

請閱讀通道用例大全來查看更多通道的使用例子。

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

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

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

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

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

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

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

在下面這個例子中,數(shù)據(jù)接收和發(fā)送操作被用在兩個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)將不斷地嘗試從一個通道接收數(shù)據(jù),直到此通道關(guān)閉并且它的緩沖隊列為空為止。 和應(yīng)用于數(shù)組/切片/映射的for-range語法不同,應(yīng)用于通道的for-range語法中最多只能出現(xiàn)一個循環(huán)變量,此循環(huán)變量用來存儲接收到的值。

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

等價于

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

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

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

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

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

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

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

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

在下面這個例子中,default分支將鐵定得到執(zhí)行,因為兩個case分支后的操作均為阻塞的。

package main

import "fmt"

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

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

package main

import "fmt"

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

下面這個程序有50%的幾率會因為恐慌而崩潰。 此程序中select-case代碼塊中的兩個case操作均不阻塞,所以隨機一個將被執(zhí)行。 如果第一個case操作(向已關(guān)閉的通道發(fā)送數(shù)據(jù))被執(zhí)行,則一個恐慌將產(chǎn)生。

package main

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

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

select-case流程控制是Go中的一個重要和獨特的特性。 下面列出了官方標準運行時中select-case流程控制的實現(xiàn)步驟。

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

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

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

更多

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

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

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


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

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號