Channel 是什么?
channel,通道,本質(zhì)上是一個(gè)通信對(duì)象,goroutine 之間可以使用它來(lái)通信。從技術(shù)上講,通道是一個(gè)數(shù)據(jù)傳輸管道,可以向通道寫(xiě)入或從中讀取數(shù)據(jù)。
定義一個(gè)channel
Go 規(guī)定: 使用
chan
關(guān)鍵字來(lái)創(chuàng)建通道,使用make
關(guān)鍵字初始化通道,一個(gè)通道只能傳輸一種類(lèi)型的數(shù)據(jù)。
上面的程序定義一個(gè)通道c
變量,它可以傳輸類(lèi)型為int的數(shù)據(jù)。上面的程序打印 <nil>, 是因?yàn)橥ǖ赖牧阒凳?nil。但是 nil 通道不能傳輸數(shù)據(jù)。因此,我們必須使用 make 函數(shù)來(lái)創(chuàng)建一個(gè)可用的通道。
我們使用簡(jiǎn)寫(xiě)語(yǔ)法 :=
和 make
函數(shù)創(chuàng)建通道。上述程序產(chǎn)生以下結(jié)果:
type of `c` is chan int
value of `c` is 0xc0420160c0
注意通道的值 c
,看起來(lái)它是一個(gè)內(nèi)存地址。
通道是指針類(lèi)型。某些場(chǎng)景下,goroutine 之間通信時(shí),會(huì)將通道作為參數(shù)傳遞給函數(shù)或方法,當(dāng)程序接收該通道作為參數(shù)時(shí),無(wú)需取消引用它即可從該通道推送或拉取數(shù)據(jù)。
通道數(shù)據(jù)讀寫(xiě)
Go規(guī)定:使用左箭頭語(yǔ)法
<-
從通道讀取和寫(xiě)入數(shù)據(jù)。
- 寫(xiě)入數(shù)據(jù)
c <- data
上面的示例表示:將data發(fā)送到通道 c 中。看箭頭的方向,它從data指向c, 因此可以想象”我們正在嘗試將data推送到 c”。
- 讀取數(shù)據(jù)
<- c
上述示例表示:從通道 c 讀取數(shù)據(jù)。看箭頭方向,從 c 通道開(kāi)始。
該語(yǔ)句不會(huì)將數(shù)據(jù)推存到任何內(nèi)容中,但它仍然是一個(gè)有效的語(yǔ)句。
如果您有一個(gè)變量data,可以保存來(lái)自通道的數(shù)據(jù),則可以使用以下語(yǔ)法:
var data int
data = <- c
現(xiàn)在來(lái)自通道 c 的 int 類(lèi)型的數(shù)據(jù)可以存儲(chǔ)到變量data中, data 必須也是int類(lèi)型。
上面的語(yǔ)法可以使用簡(jiǎn)寫(xiě)語(yǔ)法重寫(xiě),如下所示
data := <- c
golang 自動(dòng)識(shí)別c中的數(shù)據(jù)類(lèi)型,并將data設(shè)置為同樣的類(lèi)型
以上所有通道操作都是阻塞的
在上一課中,我們使用 time.Sleep 阻塞了goroutine,從而調(diào)度了其它的goroutine。 而通道操作本質(zhì)上也是阻塞的,當(dāng)向通道寫(xiě)入數(shù)據(jù)時(shí),當(dāng)前goroutine 會(huì)被阻塞,直到其它goroutine 從該通道讀取數(shù)據(jù)。我們?cè)诓l(fā)章節(jié)中看到,當(dāng)前的goroutine阻塞后, 調(diào)度器會(huì)調(diào)度其它空閑的goroutine繼續(xù)工作,從而保證程序不會(huì)永遠(yuǎn)阻塞,這是用channel來(lái)做的。通道的這個(gè)特性在 goroutines 通信中非常有用,因?yàn)樗梢苑乐刮覀兙帉?xiě)手動(dòng)鎖和 hack 來(lái)使它們彼此協(xié)同工作。
channel 實(shí)踐
下面我們一步一步的講講解上面程序的執(zhí)行過(guò)程:
- 首先聲明了greet函數(shù),它接受字符串類(lèi)型的通道c。greeter這個(gè)函數(shù)從通道 c 讀取數(shù)據(jù),并打印到控制臺(tái)。
- 在 main 函數(shù)中,第一條語(yǔ)句: 打印 "main started" 到控制臺(tái)
- main函數(shù)第二條語(yǔ)句:使用 make 初始化字符串類(lèi)型的通道 c
- main 函數(shù)第三條語(yǔ)句中: 將通道c 傳遞給 greet 函數(shù),但使用 go 關(guān)鍵字將其作為 goroutine 執(zhí)行。
- 此時(shí),進(jìn)程有 2 個(gè) goroutine,而活動(dòng)goroutine 是
main goroutine
(查看上一課就知道它是什么)。然后控制權(quán)轉(zhuǎn)到下一行代碼。 - main 第四行語(yǔ)句:將字符串值 "John" 發(fā)送到通道 c。此時(shí),goroutine 被阻塞,直到某個(gè) goroutine 讀取它。 Go調(diào)度器調(diào)度
greet goroutine
,它按照第一點(diǎn)中提到的那樣執(zhí)行。 - 然后
main goroutine
激活并執(zhí)行最后的語(yǔ)句,打印 "main stopped"。
deadlock
前面我們說(shuō)了“通道本質(zhì)上是阻塞的”,當(dāng)在通道寫(xiě)入或讀取數(shù)據(jù)時(shí),該 goroutine 會(huì)阻塞,而且會(huì)一直阻塞,控制權(quán)被傳遞給其他可用的 goroutine。如果沒(méi)有其他可用的 goroutine 怎么辦,程序無(wú)法向下執(zhí)行了,這就產(chǎn)生死鎖錯(cuò)誤,導(dǎo)致整個(gè)程序崩潰。
當(dāng)嘗試從某個(gè)通道讀取數(shù)據(jù),但通道沒(méi)有可用的值時(shí),會(huì)期望其它 goroutine 推送值到該通道, 而阻塞當(dāng)前goroutine,交出控制權(quán),因此,此讀取操作將是阻塞的。同樣,如果要向通道發(fā)送數(shù)據(jù),會(huì)交出控制權(quán)到其它goroutine,直到某個(gè) goroutine 從中讀取數(shù)據(jù)。因此,此發(fā)送操作將被阻塞。
死鎖的一個(gè)簡(jiǎn)單示例是只有主 goroutine 執(zhí)行一些通道操作。
上面的程序?qū)⒃谶\(yùn)行時(shí)死鎖了, 拋出以下錯(cuò)誤:
main() started
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
program.Go:10 +0xfd
exit status 2
fatal error: all goroutines are asleep - deadlock!
。似乎所有 goroutine 都處于睡眠狀態(tài),或者根本沒(méi)有其他 goroutine 可用于調(diào)度。
關(guān)閉channel
一個(gè)通道可以關(guān)閉,這樣就不能再通過(guò)它發(fā)送數(shù)據(jù)了。接收器 goroutine 可以使用 val, ok := <- c
語(yǔ)法找出通道的狀態(tài),如果通道打開(kāi)或可以執(zhí)行讀取操作,則 ok 為真,如果通道關(guān)閉且無(wú)法執(zhí)行更多讀取操作,則為 false 。
可以使用
close(channel)
的close
內(nèi)置函數(shù)關(guān)閉通道。
讓我們看一個(gè)簡(jiǎn)單的例子。
只是為了幫助你理解阻塞的概念,首先發(fā)送操作
c <- "John"
是阻塞的,一些 goroutine 必須從通道讀取數(shù)據(jù),因此greet goroutine
是由 Go調(diào)度器調(diào)度的。然后第一次讀取操作<-c
是非阻塞的,因?yàn)閿?shù)據(jù)存在于通道 c 中以供讀取。第二次讀取操作<-c
將被阻塞,因?yàn)橥ǖ?c 沒(méi)有任何數(shù)據(jù)可供讀取,因此 Go 調(diào)度器激活main goroutine
并且程序從close(c)
函數(shù)開(kāi)始執(zhí)行。
從上面的錯(cuò)誤中,是由嘗試在關(guān)閉的通道上發(fā)送數(shù)據(jù)引起的。為了更好地理解關(guān)閉通道的可用性,讓我們看看 for 循環(huán)。
for loop
for{} 的無(wú)限循環(huán)語(yǔ)法可用于讀取通過(guò)通道發(fā)送的多個(gè)值。
在上面的例子中,我們正在創(chuàng)建 goroutine squares,它一個(gè)一個(gè)地返回從 0 到 9 的數(shù)字的平方。在main goroutine 中,我們正在無(wú)限循環(huán)中讀取這些數(shù)字。
在無(wú)限 for 循環(huán)中,由于我們需要一個(gè)條件來(lái)在某個(gè)時(shí)刻中斷循環(huán),因此我們使用語(yǔ)法 val, ok := <-c
從通道讀取值。在這里,當(dāng)通道關(guān)閉時(shí),ok 會(huì)給我們額外的信息。因此,在 squares 協(xié)程中,在寫(xiě)完所有數(shù)據(jù)后,我們使用語(yǔ)法 close(c)
關(guān)閉通道。當(dāng) ok 為真時(shí),程序打印 val 中的值和通道狀態(tài) ok。當(dāng)它為假時(shí),我們使用 break 關(guān)鍵字跳出循環(huán)。因此,上述程序產(chǎn)生以下結(jié)果:
main() started
0 true
1 true
4 true
9 true
16 true
25 true
36 true
49 true
64 true
81 true
0 false <-- loop broke!
main() stopped
當(dāng)通道關(guān)閉時(shí),goroutine 讀取的值為通道數(shù)據(jù)類(lèi)型的零值。在這種情況下,由于 channel 正在傳輸 int 數(shù)據(jù)類(lèi)型,因此可以從結(jié)果中看到它是 0。與向通道讀取或?qū)懭胫挡煌P(guān)閉通道不會(huì)阻塞當(dāng)前的 goroutine。
為了避免手動(dòng)檢查通道關(guān)閉條件的痛苦,Go 提供了更簡(jiǎn)單的
for range
循環(huán),它會(huì)在通道關(guān)閉時(shí)自動(dòng)關(guān)閉。
讓我們修改我們之前的上述程序。
在上面的程序中,我們使用了 for val := range c
而不是 for{}
。 range 將一次從通道讀取一個(gè)值,直到它關(guān)閉。因此,上述程序產(chǎn)生以下結(jié)果
main() started
0
1
4
9
16
25
36
49
64
81
main() stopped
如果不關(guān)閉 for range 循環(huán)中的通道,程序?qū)⒃谶\(yùn)行時(shí)拋出死鎖致命錯(cuò)誤。所以,數(shù)據(jù)發(fā)送完成時(shí),要記得關(guān)閉通道;還可以使用select,default來(lái)避免這個(gè)問(wèn)題。
緩沖區(qū)通道和通道容量
正如我們所見(jiàn),每一個(gè)通道寫(xiě)入操作都會(huì)阻塞當(dāng)前goroutine。但是到目前位置,在創(chuàng)建channel時(shí),我們都沒(méi)有給make第二個(gè)參數(shù)。這第二個(gè)參數(shù)就是通道容量,或緩沖區(qū)。默認(rèn)為0的通道成為無(wú)緩沖區(qū)通道。
當(dāng)通道的緩沖區(qū)大于0時(shí),緩沖區(qū)被填滿之前, goroutine不會(huì)被阻塞。緩沖區(qū)滿后, 只有當(dāng)通道的最后數(shù)據(jù)被讀取,新的值才能被添加。有一個(gè)問(wèn)題是通道讀取是饑渴操作,這表示讀取一旦開(kāi)始,只要緩沖區(qū)有數(shù)據(jù), 讀取就會(huì)一直進(jìn)行。技術(shù)上來(lái)講,緩沖區(qū)為空時(shí),讀取操作才是阻塞的。
我們使用下面的語(yǔ)法定義帶緩沖的通道:
c := make(chan Type, n)
上面會(huì)創(chuàng)建一個(gè)數(shù)據(jù)類(lèi)型為 Type 且緩沖區(qū)大小為 n 的通道。直到 channel 收到 n+1 個(gè)發(fā)送操作,它才會(huì)阻塞當(dāng)前的 goroutine。
在上面的程序中,通道 c 的緩沖區(qū)容量為 3。這意味著它可以容納 3 個(gè)值,在第20行,由于緩沖區(qū)沒(méi)有溢出(因?yàn)槲覀儧](méi)有推送任何新值),main goroutine 不會(huì)阻塞,運(yùn)行后退出,并不會(huì)調(diào)度到squares goroutine。
讓我們?cè)诎l(fā)送一個(gè)額外的值:
如我們前面討論, 通道 c <- 4 發(fā)送操作超出了channel的容量,阻塞了main goroutine, squares goroutine 獲得控制權(quán),并讀取通道內(nèi)的所有值。
一個(gè)通道的長(zhǎng)度和容量怎么計(jì)算呢
與切片類(lèi)似,緩沖通道具有長(zhǎng)度和容量。通道的長(zhǎng)度是通道緩沖區(qū)中值的數(shù)量,而通道的容量是緩沖區(qū)大小,創(chuàng)建時(shí)n的值。計(jì)算長(zhǎng)度,我們使用len函數(shù),而找出容量,我們使用cap函數(shù),就像切片一樣
如果你想知道為什么上面的程序運(yùn)行良好并且沒(méi)有拋出死鎖錯(cuò)誤。這是因?yàn)椋捎谕ǖ廊萘繛?3 并且緩沖區(qū)中只有 2 個(gè)值可用,Go 沒(méi)有嘗試通過(guò)阻止主 goroutine 執(zhí)行來(lái)調(diào)度另一個(gè) goroutine。如果需要,您可以簡(jiǎn)單地在main goroutine 中讀取這些值,因?yàn)榧词咕彌_區(qū)未滿,也不會(huì)阻止您從通道讀取值。
另外一個(gè)例子:
使用多個(gè) goroutine
下面我們創(chuàng)建2個(gè)goroutines, 一個(gè)計(jì)算整數(shù)的平方, 一個(gè)計(jì)算整數(shù)的立方
下面分析一下程序的執(zhí)行過(guò)程:
- 首先創(chuàng)建了兩個(gè)函數(shù),
square
和cube
, 兩個(gè)函數(shù)都使用c chan int
channel 作為參數(shù), 函數(shù)從c
中讀取整數(shù), 計(jì)算完成后,寫(xiě)回c
中 - 在main goroutinue 中,我們創(chuàng)建了兩個(gè)int 類(lèi)型的 channel :
squareChan
和cubeChan
- 使用
go
關(guān)鍵字, 以goroutine的方式 square 和 cube - 此時(shí)控制權(quán)還在 main goroutine中, 我們個(gè)變量
testNum
一個(gè)值3 - 此時(shí)我們把
testNum
發(fā)送到channelsquareChan
和cubeChan
, main goroutine 將被阻塞,直到這些channel的數(shù)據(jù)被讀取。一旦chanel中的數(shù)據(jù)被讀取,main goroutine 將繼續(xù)執(zhí)行。 - 此時(shí)在main goroutine中, 嘗試從
squareChan
和cubeChan
讀取數(shù)據(jù), 這依然是阻塞操作, 直到這些channel在他們各自的goroutine被寫(xiě)入數(shù)據(jù), main gorounine 才能繼續(xù)執(zhí)行
上圖中程序的執(zhí)行結(jié)果如下:
[main] main() started
[main] sent testNum to squareChan
[square] reading
[main] resuming
[main] sent testNum to cubeChan
[cube] reading
[main] resuming
[main] reading from channels
[main] sum of square and cube of 3 is 36
[main] main() stopped
單向通道
至此, 我們所操作的channel都是雙向的, 既能讀取,也能寫(xiě)入。我們也可以創(chuàng)建單向操作的channel, 只讀channel:只能讀取數(shù)據(jù);只寫(xiě)chanel:只能寫(xiě)入數(shù)據(jù)。
單向chanel創(chuàng)建依然使用make
函數(shù),只是額外添加了單向箭頭(<-
)語(yǔ)法:
roc := make(<-chan int) // read-only chan
soc := make(chan<- int) // send-only chan
在上面的程序中, roc是只讀channel, make函數(shù)中的箭頭方向是遠(yuǎn)離chan(<-chan
); soc是只寫(xiě)channel, make函數(shù)中箭頭方向,指向chan(chan<-
), 他們是兩個(gè)不同的類(lèi)型
但是單向信道有什么用呢?使用單向通道增加了程序的類(lèi)型安全性。可減少程序出錯(cuò)概率。
假如有如下場(chǎng)景: 假如你有一個(gè)goroutine, 你只需要在其中讀取channel中的數(shù)據(jù), 但是main goroutine需要在同一個(gè)channle讀取和寫(xiě)入數(shù)據(jù),該怎么做呢?
幸運(yùn)的是go 提供了簡(jiǎn)單的語(yǔ)法, 把雙向的channle,改為單向
如上述示例所示, 我們只需要在 greet 函數(shù)中, 將接收參數(shù)修改為單向channel即可,現(xiàn)在我們?cè)趃reet中,對(duì)channel的操作,只能讀了, 任何寫(xiě)造作都會(huì)導(dǎo)致 fatal 錯(cuò)誤 "invalid operation: roc <- "some text" (send to receive-only type <-chan string)"。
匿名goroutine
在 goroutines 章節(jié)中,我們學(xué)習(xí)了匿名 goroutines。我們也可以與他們一起實(shí)施渠道。讓我們修改前面的簡(jiǎn)單示例,在匿名 goroutine 中實(shí)現(xiàn) channel。
這是我們之前的例子
下面是一個(gè)修改后的例子,我們將 greet goroutine 變成了一個(gè)匿名 goroutine。
channel 作為 channel 的數(shù)據(jù)類(lèi)型
如標(biāo)題所示, channel 作為 golang中類(lèi)型中國(guó)的一等公民, 可以像其他值一樣在任何地方使用: 作為結(jié)構(gòu)的元素, 函數(shù)參數(shù),返回值,甚至是另外一個(gè)通道的類(lèi)型。下面的例子中,我們使用一個(gè)通道作為另外一個(gè)通道的數(shù)據(jù)類(lèi)型。
select
select 就像沒(méi)有任何輸入?yún)?shù)的 switch 一樣,但它只用于通道操作。 select 語(yǔ)句用于僅對(duì)多個(gè)通道中的一個(gè)執(zhí)行操作,由 case 塊有條件地選擇。
我們先看一個(gè)例子:
從上面的程序中,可以看到select語(yǔ)句就像switch一樣,但不是布爾操作,而是通道操作。 select 語(yǔ)句是阻塞的,除非它有default項(xiàng)。 一旦滿足條件之一, 它將解除阻塞。 那么它什么時(shí)候滿足條件呢?
如果所有 case 語(yǔ)句(通道操作)都被阻塞,則 select 語(yǔ)句將等待,直到其中一個(gè) case 語(yǔ)句(其通道操作)解除阻塞,然后執(zhí)行該 case。如果部分或全部通道操作是非阻塞的,則將隨機(jī)選擇非阻塞情況之一并立即執(zhí)行。
為了解釋上面的程序,我們啟動(dòng)了 2 個(gè)具有獨(dú)立通道的 goroutine。然后啟動(dòng)了 2 個(gè)case的 select 語(yǔ)句。一種情況從 chan1 讀取值,另一種情況從 chan2 讀取值。由于這些通道是無(wú)緩沖的,讀操作將被阻塞(寫(xiě)操作也是如此)。所以這兩種選擇的情況都是阻塞的。因此 select 將等待,直到其中一種情況變?yōu)榉亲枞?/p>
當(dāng)程序運(yùn)行到 select
代碼段時(shí), main goroutine 會(huì)阻塞, 然后它將調(diào)度 select 語(yǔ)句中存在的所有 goroutine, 每次一個(gè),這個(gè)例子里面是service1
和 service2
對(duì)應(yīng)的goroutine,service1
將會(huì)等待3s,然后, 寫(xiě)入一條數(shù)據(jù)到 chan1 解除阻塞, service2
等待5s,寫(xiě)入一條數(shù)據(jù)到chan2, 然后解除阻塞。由于 service1 比 service2 更早解除阻塞,case 1 將首先解除阻塞,因此將執(zhí)行該 case,而其他 case(此處為 case 2)將被忽略。完成案例執(zhí)行后,主函數(shù)的執(zhí)行將繼續(xù)進(jìn)行。
上面的程序模擬了真實(shí)世界的 Web 服務(wù),其中負(fù)載均衡器收到數(shù)百萬(wàn)個(gè)請(qǐng)求,并且必須從可用服務(wù)之一返回響應(yīng)。使用 goroutines、channels 和 select,我們可以向多個(gè)服務(wù)請(qǐng)求響應(yīng),并且可以使用快速響應(yīng)的服務(wù)。
為了模擬所有情況何時(shí)都阻塞并且響應(yīng)幾乎同時(shí)可用,我們可以簡(jiǎn)單地刪除 Sleep 調(diào)用。
上述程序產(chǎn)生以下結(jié)果(您可能會(huì)得到不同的結(jié)果):
main() started 0s
service2() started 481μs
Response from service 2 Hello from service 2 981.1μs
main() stopped 981.1μs
有時(shí)候也可能是:
main() started 0s
service1() started 484.8μs
Response from service 1 Hello from service 1 984μs
main() stopped 984μs
發(fā)生這種情況是因?yàn)?chan1 和 chan2 操作幾乎同時(shí)發(fā)生,但執(zhí)行和調(diào)度仍然存在一些時(shí)間差。
default case
和 switch 語(yǔ)句一樣,select 語(yǔ)句也有 default 項(xiàng)。 default 是非阻塞的:default case 使得 select 語(yǔ)句總是非阻塞的。這意味著,任何通道(緩沖或非緩沖)上的發(fā)送和接收操作始終是非阻塞的。
如果某個(gè)值在任何通道上可用,則 select 將執(zhí)行該情況。如果沒(méi)有,它將立即執(zhí)行默認(rèn)情況。
在上面的程序中,由于通道是無(wú)緩沖的,并且兩個(gè)通道操作的值都不是立即可用的,因此將執(zhí)行默認(rèn)情況。如果上面的 select 語(yǔ)句沒(méi)有 default case,select 就會(huì)阻塞并且響應(yīng)會(huì)有所不同。
由于有default的情況下select 是非阻塞的,main goroutine 不會(huì)阻塞,調(diào)度程序也不會(huì)調(diào)用其他 goroutine, service1 和 service2 并不會(huì)執(zhí)行。但是我們可以手動(dòng)調(diào)用 time.Sleep 來(lái)阻塞 main goroutine。這樣,所有其他的 goroutine 都會(huì)執(zhí)行并死亡,將控制權(quán)返回給 main goroutine,它會(huì)在一段時(shí)間后喚醒。當(dāng)main gorountine喚醒時(shí),通道將立即有可用的值。
上述程序的執(zhí)行結(jié)果:
main() started 0s
service1() started 0s
service2() started 0s
Response from service 1 Hello from service 1 3.0001805s
main() stopped 3.0001805s
也可能是:
main() started 0s
service1() started 0s
service2() started 0s
Response from service 2 Hello from service 2 3.0000957s
main() stopped 3.0000957s
死鎖
當(dāng)沒(méi)有通道可用于發(fā)送或接收數(shù)據(jù)時(shí),default case
很有用。為了避免死鎖,我們可以使用 default case。這是可能的,因?yàn)槟J(rèn)情況下的所有通道操作都是非阻塞的,如果數(shù)據(jù)不是立即可用,Go 不會(huì)安排任何其他 goroutine 將數(shù)據(jù)發(fā)送到通道。
與接收類(lèi)似,在發(fā)送操作中,如果其他 goroutine 處于休眠狀態(tài)(未準(zhǔn)備好接收值),則執(zhí)行 default case。
nil channel
眾所周知,通道的默認(rèn)值為 nil。因此我們不能在 nil 通道上執(zhí)行發(fā)送或接收操作。一旦在 select 語(yǔ)句中使用 nil 通道時(shí),它會(huì)拋出以下錯(cuò)誤之一或兩個(gè)錯(cuò)誤。
從上面的結(jié)果我們可以看出,select(no cases)意味著select語(yǔ)句實(shí)際上是空的,因?yàn)楹雎粤藥в衝il channel的cases。但是由于空的 select{} 語(yǔ)句阻塞了主 goroutine 并且 service goroutine 被安排在它的位置,nil 通道上的通道操作會(huì)拋出 chan send (nil chan) 錯(cuò)誤。為了避免這種情況,我們default情況。
上面的程序不僅忽略了 case 塊,而且立即執(zhí)行了 default 語(yǔ)句。因此調(diào)度程序沒(méi)有時(shí)間來(lái)調(diào)度 service goroutine。但這真是糟糕的設(shè)計(jì)。應(yīng)該始終檢查通道的 nil 值。
添加超時(shí)
上面的程序不是很有用,因?yàn)橹粓?zhí)行default case。但有時(shí),我們想要的是任何可用的服務(wù)都應(yīng)該在理想的時(shí)間內(nèi)做出響應(yīng),如果沒(méi)有,則應(yīng)該執(zhí)行 default case。這可以通過(guò)使用在定義的時(shí)間后解除阻塞的通道操作的情況來(lái)完成。此通道操作由時(shí)間包的 After 函數(shù)提供。讓我們看一個(gè)例子。
上面的程序,在 2 秒后產(chǎn)生以下結(jié)果。
main() started 0s
No response received 2.0010958s
main() stopped 2.0010958s
在上面的程序中, <-time.After(2 * time.Second)
2s 后解除阻塞,并返回時(shí)間,但在這里,我們對(duì)其返回值不感興趣。由于它也像一個(gè) goroutine,我們有 3 個(gè) goroutine, time.After 是第一個(gè)解除阻塞的channel。因此,對(duì)應(yīng)于該 goroutine 操作的 case 被執(zhí)行。
這很有用,因?yàn)槟幌氲却齺?lái)自可用服務(wù)的響應(yīng)太長(zhǎng)時(shí)間,因?yàn)橛脩舯仨毜却荛L(zhǎng)時(shí)間才能從服務(wù)中獲取任何信息。如果我們?cè)谏厦娴睦又刑砑?10 * time.Second,來(lái)自 service1 的響應(yīng)將被打印出來(lái),我想現(xiàn)在很明顯了。
empty select
與 for{} 空循環(huán)一樣,空的 select{} 語(yǔ)法也是有效的,但有一個(gè)問(wèn)題。{}正如我們所知,select 語(yǔ)句會(huì)被阻塞,直到其中一個(gè) case 解除阻塞,并且由于沒(méi)有可用的 case 語(yǔ)句來(lái)解除阻塞,主 goroutine 將永遠(yuǎn)阻塞,從而導(dǎo)致死鎖。
在上面的程序中,我們知道 select 會(huì)阻塞 main goroutine,調(diào)度器會(huì)調(diào)度另一個(gè)可用的 goroutine,即 service。但在那之后,它會(huì)死掉,調(diào)度必須調(diào)度另一個(gè)可用的 goroutine,但由于主例程被阻塞,沒(méi)有其他 goroutine 可用,導(dǎo)致死鎖。
main() started
Hello from service!
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [select (no cases)]:
main.main()
program.Go:16 +0xba
exit status 2
waitgroup
讓我們想象一種情況,您需要知道所有 goroutine 是否都完成了它們的工作。這與 select 你只需要一個(gè)條件為真的情況有點(diǎn)相反,但在這里你需要所有條件都為真才能解除main goroutine 的阻塞。這里的條件是通道操作成功。
WaitGroup 是一個(gè)帶有計(jì)數(shù)器值的結(jié)構(gòu)體,它跟蹤產(chǎn)生了多少 goroutine 以及有多少已經(jīng)完成了它們的工作。當(dāng)這個(gè)計(jì)數(shù)器達(dá)到零時(shí),意味著所有的 goroutine 都完成了他們的工作。
讓我們深入研究一個(gè)示例:
在上面的程序中,創(chuàng)建了一個(gè)類(lèi)型為 sync.WaitGroup
的空結(jié)構(gòu)(帶有零值字段)wg。 WaitGroup 結(jié)構(gòu)體有禁止導(dǎo)出的字段,例如 noCopy、state1 和 sema,我們不需要知道它們的內(nèi)部實(shí)現(xiàn)。這個(gè)結(jié)構(gòu)有三個(gè)方法,即 Add
, Wait
和 Done
。
Add
方法需要一個(gè) int 參數(shù),它是 WaitGroup 計(jì)數(shù)器的增加量。計(jì)數(shù)器只不過(guò)是一個(gè)默認(rèn)值為 0 的整數(shù)。它保存了正在運(yùn)行的 goroutine 的數(shù)量。當(dāng) WaitGroup 創(chuàng)建時(shí),它的計(jì)數(shù)器值為 0,我們可以通過(guò)使用 Add 方法傳遞 delta 作為參數(shù)來(lái)增加它。請(qǐng)記住,當(dāng) goroutine 啟動(dòng)時(shí),計(jì)數(shù)器不會(huì)遞增,因此我們需要手動(dòng)遞增它。
Wait
方法用于阻塞當(dāng)前的gorountine。一旦計(jì)數(shù)器達(dá)到 0,該 goroutine 將解除阻塞。因此,我們需要一些東西來(lái)減少計(jì)數(shù)器。
Done
方法就是用來(lái)遞減計(jì)數(shù)器的。它不接受任何參數(shù),因此它只將計(jì)數(shù)器減 1。
在上面的程序中,創(chuàng)建 wg 后,我們運(yùn)行了 3 次 for 循環(huán)。在每一輪中,我們啟動(dòng)了 1 個(gè) goroutine 并將計(jì)數(shù)器加 1。這意味著,現(xiàn)在我們有 3 個(gè) goroutine 等待執(zhí)行,WaitGroup 計(jì)數(shù)器為 3。請(qǐng)注意,我們?cè)?goroutine 中傳遞了一個(gè)指向 wg 的指針。這是因?yàn)樵?goroutine 中,一旦我們完成了 goroutine 應(yīng)該做的任何事情,我們需要調(diào)用 Done 方法來(lái)遞減計(jì)數(shù)器。如果 wg 作為值傳遞,則 main 中的 wg 不會(huì)遞減。這是很明顯的。
在 for 循環(huán)執(zhí)行完畢后,我們?nèi)匀粵](méi)有將控制權(quán)交給其他 goroutine。這是通過(guò)調(diào)用wg 的 Wait方法來(lái)完成的。這將阻塞 main goroutine,直到計(jì)數(shù)器達(dá)到 0。一旦計(jì)數(shù)器達(dá)到 0,因?yàn)閺?3 個(gè)協(xié)程開(kāi)始,我們?cè)?wg 上調(diào)用了 Done 方法 3 次,主協(xié)程將解除阻塞并開(kāi)始執(zhí)行進(jìn)一步的代碼。
上面的程序?qū)a(chǎn)生如下結(jié)果:
main() started
Service called on instance 2
Service called on instance 3
Service called on instance 1
main() stopped
以上結(jié)果對(duì)你們來(lái)說(shuō)可能不同,因?yàn)?goroutine 的執(zhí)行順序可能會(huì)有所不同。
添加方法接受 int 類(lèi)型,這意味著 delta 也可以是負(fù)數(shù)。要了解更多信息,請(qǐng)查看官方文檔[https://golang.org/pkg/sync/#WaitGroup.Add]。
worker pool
顧名思義,worker pool
:即工作池,是并發(fā)模式協(xié)同完成同樣工作一些列 goroutine 的集合。在前面 WaitGroup 中,我們看到一組 goroutines 并發(fā)工作,但它們沒(méi)有特定的工作內(nèi)容。一旦將channel加入其中,讓它們有相同的工作去完成,這些goroutines就會(huì)成為一個(gè)工作池。
因此,worker pool
背后的概念是維護(hù)一個(gè) worker goroutines 池,它接收一些任務(wù)并返回結(jié)果。一旦他們都完成了他們的工作,我們就會(huì)收集結(jié)果。所有這些 goroutine 都出于各自的目的使用相同的通道。
讓我們看一個(gè)帶有兩個(gè)通道的簡(jiǎn)單示例,即 tasks 和 results
這個(gè)程序發(fā)生了什么呢?
-
sqrWorker
是一個(gè)工作函數(shù), 它接收三個(gè)參數(shù)tasks
channel,results
channel 和id
, 這個(gè)gorountine的任務(wù)是接收tasks
中的數(shù)據(jù),計(jì)算平方,并把結(jié)果發(fā)送到results
中。
- 在 main 函數(shù)中, 創(chuàng)建了緩沖容量為10的
tasks
和results
channel,因此,在tasks
緩沖滿之前, 發(fā)送操作都是非阻塞的。因此,設(shè)置大的緩沖值是一個(gè)是一個(gè)好主意。
- 然后我們生成多個(gè)
sqrWorker
goroutines實(shí)例,把前面建立的兩個(gè)channel 和 id 作為參數(shù),其中id是作為標(biāo)記,標(biāo)識(shí)是哪個(gè)gorountine正在執(zhí)行任務(wù)。
- 然后我們將 5 個(gè)job傳遞給非阻塞的
tasks
channel。
- 完成了發(fā)送job到
tasks
后,我們關(guān)閉了它。這并不是必需的,但是如果出現(xiàn)一些錯(cuò)誤,它將在將來(lái)節(jié)省大量時(shí)間。
- 然后使用 for, 循環(huán) 5 次,我們從
results
channel中提取數(shù)據(jù)。由于對(duì)空緩沖區(qū)的讀取操作是阻塞的,因此將從工作池中調(diào)度一個(gè) goroutine。在 goroutine 返回一些結(jié)果之前,main goroutine 將被阻塞。
- 因?yàn)樵?worker goroutine 中模擬阻塞操作,調(diào)度程序?qū)⒄{(diào)用另一個(gè)可用的 goroutine,直到work gorountine變得可用時(shí),它會(huì)將計(jì)算結(jié)果寫(xiě)入
results
channel。由于在緩沖區(qū)滿之前,寫(xiě)入通道是非阻塞的,因此在此處寫(xiě)入results
通道是非阻塞的。此外,雖然當(dāng)前的 worker goroutine 不可用,但執(zhí)行了多個(gè)其他 worker goroutines,消耗了tasks
緩沖區(qū)中的值。在所有worker goroutine 消耗完tasks
后,tasks
通道緩沖區(qū)為空,for range 循環(huán)結(jié)束。當(dāng)tasks
通道關(guān)閉時(shí),它不會(huì)拋出死鎖錯(cuò)誤。
- 有時(shí),所有工作協(xié)程都可能處于休眠狀態(tài),因此主協(xié)程將喚醒并工作,直到結(jié)果通道緩沖區(qū)再次為空。
- 在所有工作 goroutine 死后,main goroutine 將重新獲得控制權(quán)并從結(jié)果通道打印剩余的結(jié)果并繼續(xù)執(zhí)行。
上面的例子解釋了多個(gè) goroutines 如何可以在同一個(gè)通道上提供數(shù)據(jù)并優(yōu)雅地完成工作。當(dāng)worker被阻塞時(shí),goroutines 很靈活的解決了這個(gè)問(wèn)題。如果刪除 time.Sleep() 的調(diào)用,則只有一個(gè) goroutine 獨(dú)自完成該作業(yè),因?yàn)樵?for range 循環(huán)完成且 goroutine 終止之前不會(huì)調(diào)度其他 goroutine。
運(yùn)行系統(tǒng)速度不同, 您可能得到與上述例子不同的結(jié)果,因?yàn)槿绻械膅orountine即使是被阻塞很短的時(shí)間, main gorountine也會(huì)被喚醒執(zhí)行程序。
現(xiàn)在,讓我們使用sync.WaitGroup 實(shí)現(xiàn)相同的效果,但更優(yōu)雅。
上面的結(jié)果看起來(lái)很整潔,因?yàn)閙ain goroutine 中results
channel 上的讀取操作是非阻塞的,而results
channel 開(kāi)始讀取前已經(jīng)完成結(jié)果填充,而main goroutine 被 wg.Wait() 調(diào)用阻塞。使用 waitGroup,我們可以防止大量(不必要的)上下文切換(調(diào)度),這里是 7,而前面的例子是 9。但是有一個(gè)犧牲,因?yàn)槟惚仨毜鹊剿械墓ぷ鞫纪瓿伞?/p>
metux
Mutex【鎖】 是 Go 中最簡(jiǎn)單的概念之一。但在解釋之前,讓我們先了解什么是競(jìng)態(tài)條件。 goroutines 有它們獨(dú)立的堆棧,因此它們之間不共享任何數(shù)據(jù)。但是可能存在堆中的某些數(shù)據(jù)在多個(gè) goroutine 之間共享的情況。在這種情況下,多個(gè) goroutine 試圖在同一內(nèi)存位置操作數(shù)據(jù),從而導(dǎo)致意外結(jié)果。下面展示一個(gè)簡(jiǎn)單的例子:
在上面的程序中,我們生成了 1000 個(gè) goroutines,它們?cè)黾恿顺跏紴?0 的全局變量 i 的值。由于我們正在實(shí)現(xiàn) WaitGroup,我們希望所有 1000 個(gè) goroutines 一一增加 i 的值,從而得到 i 的最終值為 1000。當(dāng)主 goroutine 在 wg.Wait() 調(diào)用后再次開(kāi)始執(zhí)行時(shí),我們正在打印 i。讓我們看看最終的結(jié)果。
value of i after 1000 operations is 937
什么?為什么我們不到1000?看起來(lái)有些 goroutines 不起作用。但實(shí)際上,我們的程序存在競(jìng)爭(zhēng)條件。讓我們看看可能發(fā)生了什么。
i = i + 1 calculation has 3 steps
-(1) 獲取 i 當(dāng)前值
-(2) 將i的值增加 1
-(3) 使用新值替換 i
讓我們想象一個(gè)場(chǎng)景,在這些步驟之間安排了不同的 goroutine。例如,讓我們考慮 1000 個(gè) goroutine 池中的 2 個(gè) goroutine,即:G1 和 G2。
當(dāng) i 為 0 時(shí) G1 首先啟動(dòng),運(yùn)行前 2 個(gè)步驟,現(xiàn)在 i 現(xiàn)在為 1。但在 G1 更新步驟 3 中 i 的值之前,新的 goroutine G2 被調(diào)度并運(yùn)行所有步驟。但是在 G2 的情況下,i 的值仍然是 0,因此在它執(zhí)行第 3 步之后,i 將是 1。現(xiàn)在 G1 再次被安排完成第 3 步并從第 2 步更新 i 的值為 1。在 goroutines 的完美世界中在完成所有 3 個(gè)步驟后安排,2 個(gè) goroutines 的成功操作會(huì)產(chǎn)生 i 的值是 2 但這里不是這種情況。因此,我們幾乎可以推測(cè)為什么我們的程序沒(méi)有將 i 的值變?yōu)?1000。
到目前為止,我們了解到 goroutine 是協(xié)作調(diào)度的。除非一個(gè) goroutine 在并發(fā)課程中提到的條件之一阻塞,否則另一個(gè) goroutine 不會(huì)取代它。既然 i = i + 1 沒(méi)有阻塞,為什么 Go 調(diào)度器會(huì)調(diào)度另一個(gè) goroutine?
你絕對(duì)應(yīng)該在stackoverflow上查看這個(gè)答案。在任何情況下,您都不應(yīng)該依賴 Go 的調(diào)度算法并實(shí)現(xiàn)自己的邏輯來(lái)同步不同的 goroutine。
確保一次只有一個(gè) goroutine 完成上述所有 3 個(gè)步驟的一種方法是實(shí)現(xiàn)互斥鎖。 Mutex(互斥)是編程中的一個(gè)概念,其中一次只有一個(gè)例程(線程)可以執(zhí)行多個(gè)操作。這是通過(guò)一個(gè)例程獲取對(duì)值的鎖定,對(duì)它必須執(zhí)行的值進(jìn)行任何操作,然后釋放鎖定來(lái)完成的。當(dāng)值被鎖定時(shí),沒(méi)有其他例程可以讀取或?qū)懭胨?/p>
在 Go 中,互斥鎖數(shù)據(jù)結(jié)構(gòu)(本質(zhì)上是個(gè)map)由sync包提供的。在 Go 中,在對(duì)可能導(dǎo)致競(jìng)爭(zhēng)條件的值執(zhí)行任何操作之前,我們使用 mutex.Lock() 方法獲取鎖,然后是操作代碼。一旦我們完成了操作,在上面的程序 i = i + 1 中,我們使用 mutext.Unlock() 方法解鎖它。當(dāng)任何其他 goroutine 在鎖存在時(shí)嘗試讀取或?qū)懭?i 的值時(shí),該 goroutine 將阻塞,直到操作從第一個(gè) goroutine 解鎖。因此只有 1 個(gè) goroutine 可以讀取或?qū)懭?i 的值,避免競(jìng)爭(zhēng)條件。請(qǐng)記住,在整個(gè)操作被解鎖之前,鎖定和解鎖之間的操作中存在的任何變量將不可用于其他 goroutine。
讓我們用互斥鎖修改前面的例子。
在上面的程序中,我們創(chuàng)建了一個(gè)互斥鎖 m 并將指向它的指針傳遞給所有生成的 goroutine。在開(kāi)始對(duì) i 進(jìn)行操作之前,我們使用 m.Lock() 語(yǔ)法獲取了互斥鎖 m 上的鎖,并且在操作之后,我們使用 m.Unlock() 語(yǔ)法將其解鎖。以上程序產(chǎn)生以下結(jié)果
value of i after 1000 operations is 1000
從上面的結(jié)果可以看出,互斥鎖幫助我們解決了競(jìng)態(tài)條件。但是第一條規(guī)則是避免 goroutine 之間共享資源。
您可以在運(yùn)行 Go run -race program.Go 之類(lèi)的程序時(shí)使用種族標(biāo)志測(cè)試 Go 中的競(jìng)爭(zhēng)條件。在此處閱讀有關(guān)比賽檢測(cè)器的更多信息。
Concurrency Patterns 【并發(fā)模式】
通俗來(lái)講,就是日常使用的常用范式。
以下是一些可以使程序更快更可靠的概念和方法。
- Generator 【生成器模式】
使用通道,可以實(shí)現(xiàn)更好實(shí)現(xiàn)生成器。
比如斐波那契數(shù)列,計(jì)算上開(kāi)銷(xiāo)很大, 我們可以提前計(jì)算好結(jié)果,并放入channel中, 等待程序執(zhí)行到此,直接取結(jié)果即可, 而不必等待。
此圖中,調(diào)用fib 函數(shù),返回了一個(gè)channel,通過(guò)循環(huán)channel,可以接收到的計(jì)算好的數(shù)據(jù)。在 fib 函數(shù)內(nèi)部,必須返回一個(gè)只接收通道,我們創(chuàng)建一個(gè)有buffer的通道,并在函數(shù)最后返回它。fib的返回值會(huì)將這個(gè)雙向通道轉(zhuǎn)換為單向只接收通道。在匿名 goroutine 中,使用 for 循環(huán)將斐波那契數(shù)推送到此通道,完成后,關(guān)閉此通道。在主協(xié)程中,使用 fib 函數(shù)調(diào)用的范圍,我們可以直接訪問(wèn)這個(gè)通道。
- fan-in & fan-out 【多路復(fù)用】
fan-in 是一種多路復(fù)用策略,將過(guò)個(gè)輸入通道組合,以產(chǎn)生輸出通道。fan-out 是將單個(gè)通道拆分為多個(gè)通道的解復(fù)用策略。
package main
import (
"fmt"
"sync"
)
// return channel for input numbers
func getInputChan() <-chan int {
// make return channel
input := make(chan int, 100)
// sample numbers
numbers := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
// run goroutine
go func() {
for num := range numbers {
input <- num
}
// close channel once all numbers are sent to channel
close(input)
}()
return input
}
// returns a channel which returns square of numbers
func getSquareChan(input <-chan int) <-chan int {
// make return channel
output := make(chan int, 100)
// run goroutine
go func() {
// push squares until input channel closes
for num := range input {
output <- num * num
}
// close output channel once for loop finishesh
close(output)
}()
return output
}
// returns a merged channel of `outputsChan` channels
// this produce fan-in channel
// this is veriadic function
func merge(outputsChan ...<-chan int) <-chan int {
// create a WaitGroup
var wg sync.WaitGroup
// make return channel
merged := make(chan int, 100)
// increase counter to number of channels `len(outputsChan)`
// as we will spawn number of goroutines equal to number of channels received to merge
wg.Add(len(outputsChan))
// function that accept a channel (which sends square numbers)
// to push numbers to merged channel
output := func(sc <-chan int) {
// run until channel (square numbers sender) closes
for sqr := range sc {
merged <- sqr
}
// once channel (square numbers sender) closes,
// call `Done` on `WaitGroup` to decrement counter
wg.Done()
}
// run above `output` function as groutines, `n` number of times
// where n is equal to number of channels received as argument the function
// here we are using `for range` loop on `outputsChan` hence no need to manually tell `n`
for _, optChan := range outputsChan {
go output(optChan)
}
// run goroutine to close merged channel once done
go func() {
// wait until WaitGroup finishesh
wg.Wait()
close(merged)
}()
return merged
}
func main() {
// step 1: get input numbers channel
// by calling `getInputChan` function, it runs a goroutine which sends number to returned channel
chanInputNums := getInputChan()
// step 2: `fan-out` square operations to multiple goroutines
// this can be done by calling `getSquareChan` function multiple times where individual function call returns a channel which sends square of numbers provided by `chanInputNums` channel
// `getSquareChan` function runs goroutines internally where squaring operation is ran concurrently
chanOptSqr1 := getSquareChan(chanInputNums)
chanOptSqr2 := getSquareChan(chanInputNums)
// step 3: fan-in (combine) `chanOptSqr1` and `chanOptSqr2` output to merged channel
// this is achieved by calling `merge` function which takes multiple channels as arguments
// and using `WaitGroup` and multiple goroutines to receive square number, we can send square numbers
// to `merged` channel and close it
chanMergedSqr := merge(chanOptSqr1, chanOptSqr2)
// step 4: let's sum all the squares from 0 to 9 which should be about `285`
// this is done by using `for range` loop on `chanMergedSqr`
sqrSum := 0
// run until `chanMergedSqr` or merged channel closes
// that happens in `merge` function when all goroutines pushing to merged channel finishes
// check line no. 86 and 87
for num := range chanMergedSqr {
sqrSum += num
}
// step 5: print sum when above `for loop` is done executing which is after `chanMergedSqr` channel closes
fmt.Println("Sum of squares between 0-9 is", sqrSum)
}
完