通道是Go中的一種一等公民類型。它是Go的招牌特性之一。 和另一個(gè)招牌特性協(xié)程一起,這兩個(gè)招牌特性使得使用Go進(jìn)行并發(fā)編程(concurrent programming)變得十分方便和有趣,并且大大降低了并發(fā)編程的難度。
通道的主要作用是用來實(shí)現(xiàn)并發(fā)同步。 本篇文章將列出所有的和通道相關(guān)的概念、語法和規(guī)則。為了更好地理解通道,本文也對通道的可能的內(nèi)部實(shí)現(xiàn)略加介紹。
本篇文章中的信息量對于Go初學(xué)者來說可能有些密集。本文的某些段落可能需要反復(fù)閱讀幾遍才能有效吸收、消化和理解。
Go語言設(shè)計(jì)團(tuán)隊(duì)的首任負(fù)責(zé)人Rob Pike對并發(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ù)競爭,但不會防止程序員因?yàn)榉稿e(cuò)而寫出錯(cuò)誤的并發(fā)代碼的情況發(fā)生。
盡管Go也支持幾種傳統(tǒng)的數(shù)據(jù)同步技術(shù),但是只有通道為一等公民。 通道是Go中的一種類型,所以我們可以無需引進(jìn)任何代碼包就可以使用通道。 幾種傳統(tǒng)的數(shù)據(jù)同步技術(shù)提供在sync
和sync/atomic
標(biāo)準(zhǔn)庫包中。
實(shí)事求是地說,每種并發(fā)同步技術(shù)都有它們各自的最佳應(yīng)用場景,但是通道的應(yīng)用范圍更廣。 使用通道來做同步常常可以使得代碼看上去更整潔和易于理解。
通道的一個(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)部通道對象。 比較這兩個(gè)通道的結(jié)果為true
。
Go中有五種通道相關(guān)的操作。假設(shè)一個(gè)通道(值)為ch
,下面列出了這五種操作的語法或者函數(shù)調(diào)用。
close
來關(guān)閉一個(gè)通道:
close(ch)
傳給close
函數(shù)調(diào)用的實(shí)參必須為一個(gè)通道值,并且此通道值不能為單向接收的。
ch
發(fā)送一個(gè)值v
:
ch <- v
v
必須能夠賦值給通道ch
的元素類型。 ch
不能為單向接收通道。 <-
稱為數(shù)據(jù)發(fā)送操作符。
ch
接收一個(gè)值:
<-ch
如果一個(gè)通道操作不永久阻塞,它總會返回至少一個(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
cap(ch)
其中cap
是一個(gè)已經(jīng)在容器類型一文中介紹過的內(nèi)置函數(shù)。 cap
的返回值的類型為內(nèi)置類型int
。
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零值通道,則cap
和len
函數(shù)調(diào)用都返回0
。 這兩個(gè)操作是如此簡單,所以后面將不再對它們進(jìn)行詳解。 事實(shí)上,這兩個(gè)操作在實(shí)踐中很少使用。
通道的發(fā)送、接收和關(guān)閉操作將在下一節(jié)得到詳細(xì)解釋。
為了讓解釋簡單清楚,在本文后續(xù)部分,通道將被歸為三類:
下表簡單地描述了三種通道操作施加到三類通道的結(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) |
對于上表中的五種未打上標(biāo)的情形,規(guī)則很簡單:
下面將詳細(xì)解釋其它四種被打了上標(biāo)(A/B/C/D)的情形。
為了更好地理解通道和為了后續(xù)講解方便,先了解一下通道類型的大致內(nèi)部實(shí)現(xiàn)是很有幫助的。
我們可以認(rèn)為一個(gè)通道內(nèi)部維護(hù)了三個(gè)隊(duì)列(均可被視為先進(jìn)先出隊(duì)列):
每個(gè)通道內(nèi)部維護(hù)著一個(gè)互斥鎖用來在各種通道操作中防止數(shù)據(jù)競爭。
通道操作情形A: 當(dāng)一個(gè)協(xié)程R
嘗試從一個(gè)非零且尚未關(guān)閉的通道接收數(shù)據(jù)的時(shí)候,此協(xié)程R
將首先嘗試獲取此通道的鎖,成功之后將執(zhí)行下列步驟,直到其中一個(gè)步驟的條件得到滿足。
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)行,不會阻塞。對于這種情況,此數(shù)據(jù)接收操作為一個(gè)非阻塞操作。R
接收。此發(fā)送協(xié)程將恢復(fù)至運(yùn)行狀態(tài)。 接收數(shù)據(jù)協(xié)程R
繼續(xù)運(yùn)行,不會阻塞。對于這種情況,此數(shù)據(jù)接收操作為一個(gè)非阻塞操作。R
將被推入接收數(shù)據(jù)協(xié)程隊(duì)列,并進(jìn)入阻塞狀態(tài)。 它以后可能會被另一個(gè)發(fā)送數(shù)據(jù)協(xié)程喚醒而恢復(fù)運(yùn)行。 對于這種情況,此數(shù)據(jù)接收操作為一個(gè)阻塞操作。 通道操作情形B: 當(dāng)一個(gè)協(xié)程S
嘗試向一個(gè)非零且尚未關(guān)閉的通道發(fā)送數(shù)據(jù)的時(shí)候,此協(xié)程S
將首先嘗試獲取此通道的鎖,成功之后將執(zhí)行下列步驟,直到其中一個(gè)步驟的條件得到滿足。
S
發(fā)送的值。此接收協(xié)程將恢復(fù)至運(yùn)行狀態(tài)。 發(fā)送數(shù)據(jù)協(xié)程S
繼續(xù)運(yùn)行,不會阻塞。對于這種情況,此數(shù)據(jù)發(fā)送操作為一個(gè)非阻塞操作。S
欲發(fā)送的值將被推入緩沖隊(duì)列,發(fā)送數(shù)據(jù)協(xié)程S
繼續(xù)運(yùn)行,不會阻塞。 對于這種情況,此數(shù)據(jù)發(fā)送操作為一個(gè)非阻塞操作。S
將被推入發(fā)送數(shù)據(jù)協(xié)程隊(duì)列,并進(jìn)入阻塞狀態(tài)。 它以后可能會被另一個(gè)接收數(shù)據(jù)協(xié)程喚醒而恢復(fù)運(yùn)行。 對于這種情況,此數(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í)行:
-race
)打開時(shí),Go官方標(biāo)準(zhǔn)運(yùn)行時(shí)將很可能會對并發(fā)地關(guān)閉一個(gè)通道和向此通道發(fā)送數(shù)據(jù)這種情形報(bào)告成數(shù)據(jù)競爭。注意:當(dāng)一個(gè)緩沖隊(duì)列不為空的通道被關(guān)閉之后,它的緩沖隊(duì)列不會被清空,其中的數(shù)據(jù)仍然可以被后續(xù)的數(shù)據(jù)接收操作所接收到。詳見下面的對情形D的解釋。
通道操作情形D: 一個(gè)非零通道被關(guān)閉之后,此通道上的后續(xù)數(shù)據(jù)接收操作將永不會阻塞。 此通道的緩沖隊(duì)列中存儲數(shù)據(jù)仍然可以被接收出來。 伴隨著這些接收出來的緩沖數(shù)據(jù)的第二個(gè)可選返回(類型不確定布爾)值仍然是true
。 一旦此緩沖隊(duì)列變?yōu)榭?,后續(xù)的數(shù)據(jù)接收操作將永不阻塞并且總會返回此通道的元素類型的零值和值為false
的第二個(gè)可選返回結(jié)果。 上面已經(jīng)提到了,一個(gè)接收操作的第二個(gè)可選返回(類型不確定布爾)結(jié)果表示一個(gè)接收到的值是否是在此通道被關(guān)閉之前發(fā)送的。
如果此返回值為false
,則第一個(gè)返回值必然是一個(gè)此通道的元素類型的零值。
知道哪些通道操作是阻塞的和哪些是非阻塞的對正確理解后面將要介紹的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í)它會被從相應(yīng)的select
控制流程中的相關(guān)的若干通道的協(xié)程隊(duì)列中移除掉。
根據(jù)上面的解釋,我們可以得出如下的關(guān)于一個(gè)通道的內(nèi)部的三個(gè)隊(duì)列的各種事實(shí):
來看一些通道的使用例子來加深一下對上一節(jié)中的解釋的理解。
一個(gè)簡單的通過一個(gè)非緩沖通道實(shí)現(xiàn)的請求/響應(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 // 永久阻塞在此
}
請閱讀通道用例大全來查看更多通道的使用例子。
在一個(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ù)制。
對于官方標(biāo)準(zhǔn)編譯器,最大支持的通道的元素類型的尺寸為65535
。 但是,一般說來,為了在數(shù)據(jù)傳遞過程中避免過大的復(fù)制成本,我們不應(yīng)該使用尺寸很大的通道元素類型。 如果欲傳送的值的尺寸較大,應(yīng)該改用指針類型做為通道的元素類型。
注意,一個(gè)通道被其發(fā)送數(shù)據(jù)協(xié)程隊(duì)列和接收數(shù)據(jù)協(xié)程隊(duì)列中的所有協(xié)程引用著。因此,如果一個(gè)通道的這兩個(gè)隊(duì)列只要有一個(gè)不為空,則此通道肯定不會被垃圾回收。 另一方面,如果一個(gè)協(xié)程處于一個(gè)通道的某個(gè)協(xié)程隊(duì)列之中,則此協(xié)程也肯定不會被垃圾回收,即使此通道僅被此協(xié)程所引用。 事實(shí)上,一個(gè)協(xié)程只有在退出后才能被垃圾回收。
數(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
循環(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)變量用來存儲接收到的值。
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)
}
Go中有一個(gè)專門為通道設(shè)計(jì)的select-case
分支流程控制語法。 此語法和switch-case
分支流程控制語法很相似。 比如,select-case
流程控制代碼塊中也可以有若干case
分支和最多一個(gè)default
分支。 但是,這兩種流程控制也有很多不同點(diǎn)。在一個(gè)select-case
流程控制中,
select
關(guān)鍵字和{
之間不允許存在任何表達(dá)式和語句。fallthrough
語句不能被使用.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í)行此操作對應(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ā)送失敗,但不會阻塞。
// 下面這兩行將接收成功。
fmt.Println(tryReceive()) // Hello!
fmt.Println(tryReceive()) // Hi!
// 下面這行將接收失敗。
fmt.Println(tryReceive()) // -
}
下面這個(gè)程序有50%的幾率會因?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
流程控制是Go中的一個(gè)重要和獨(dú)特的特性。 下面列出了官方標(biāo)準(zhǔn)運(yùn)行時(shí)中select-case
流程控制的實(shí)現(xiàn)步驟。
case
操作中涉及到的通道表達(dá)式和發(fā)送值表達(dá)式按照從上到下,從左到右的順序一一估值。 在賦值語句中做為源值的數(shù)據(jù)接收操作對應(yīng)的目標(biāo)值在此時(shí)刻不需要被估值。default
分支總是排在最后。 所有case
操作中相關(guān)的通道可能會有重復(fù)的。case
操作中相關(guān)的通道進(jìn)行排序。 排序依據(jù)并不重要,官方Go標(biāo)準(zhǔn)編譯器使用通道的地址順序進(jìn)行排序。 排序結(jié)果中前N
個(gè)通道不存在重復(fù)的情況。 N
為所有case
操作中涉及到的不重復(fù)的通道的數(shù)量。 下面,通道鎖順序是針對此排序結(jié)果中的前N
個(gè)通道來說的,通道鎖逆序是指此順序的逆序。case
分支并且相應(yīng)的通道操作是一個(gè)向關(guān)閉了的通道發(fā)送數(shù)據(jù)操作,則按照通道鎖逆序解鎖所有的通道并在當(dāng)前協(xié)程中產(chǎn)生一個(gè)恐慌。 跳到第12步。case
分支并且相應(yīng)的通道操作是非阻塞的,則按照通道鎖逆序解鎖所有的通道并執(zhí)行相應(yīng)的case
分支代碼塊。 (此相應(yīng)的通道操作可能會喚醒另一個(gè)處于阻塞狀態(tài)的協(xié)程。) 跳到第12步。default
分支,則按照通道鎖逆序解鎖所有的通道并執(zhí)行此default
分支代碼塊。 跳到第12步。default
分支肯定是不存在的,并且所有的case
操作均為阻塞的。)case
分支信息)推入到每個(gè)case
操作中對應(yīng)的通道的發(fā)送數(shù)據(jù)協(xié)程隊(duì)列或接收數(shù)據(jù)協(xié)程隊(duì)列中。 當(dāng)前協(xié)程可能會被多次推入到同一個(gè)通道的這兩個(gè)隊(duì)列中,因?yàn)槎鄠€(gè)case
操作中對應(yīng)的通道可能為同一個(gè)。select-case
流程中)肯定有一個(gè)相應(yīng)case
操作與之配合傳遞數(shù)據(jù)。 在此配合過程中,當(dāng)前協(xié)程將從相應(yīng)case
操作相關(guān)的通道的接收/發(fā)送數(shù)據(jù)協(xié)程隊(duì)列中彈出。case
操作中對應(yīng)的通道的發(fā)送數(shù)據(jù)協(xié)程隊(duì)列或接收數(shù)據(jù)協(xié)程隊(duì)列中(可能以非彈出的方式)移除。
case
分支已經(jīng)在第9步中知曉。 按照通道鎖逆序解鎖所有的通道并執(zhí)行此case
分支代碼塊。從此實(shí)現(xiàn)中,我們得知
select-case
流程控制中并在以后被喚醒時(shí),它可能會從多個(gè)通道的發(fā)送數(shù)據(jù)協(xié)程隊(duì)列和接收數(shù)據(jù)協(xié)程隊(duì)列中被移除。
我們可以在通道用例大全一文中找到更多通道的使用例子。
盡管通道可以幫助我們輕松地寫出正確的并發(fā)代碼,和其它并發(fā)同步技術(shù)一樣,通道并不會阻止我們寫出不正確的并發(fā)代碼。
通道并非在任何場合總是最佳的并發(fā)同步方案,請閱讀其它并發(fā)同步技術(shù)和原子操作來了解Go中支持的更多的并發(fā)同步技術(shù)。
更多建議: