Go语言并发与并行
首先,并行!=并發(fā), 兩者是不同的
Go語言的goroutines、信道和死鎖
goroutine
Go語言中有個概念叫做goroutine, 這類似我們熟知的線程,但是更輕。
以下的程序,我們串行地去執(zhí)行兩次loop函數(shù):
毫無疑問,輸出會是這樣的:
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9下面我們把一個loop放在一個goroutine里跑,我們可以使用關鍵字go來定義并啟動一個goroutine:
func main() {go loop() // 啟動一個goroutineloop() }這次的輸出變成了:
0 1 2 3 4 5 6 7 8 9可是為什么只輸出了一趟呢?明明我們主線跑了一趟,也開了一個goroutine來跑一趟啊。
原來,在goroutine還沒來得及跑loop的時候,主函數(shù)已經(jīng)退出了。
main函數(shù)退出地太快了,我們要想辦法阻止它過早地退出,一個辦法是讓main等待一下:
func main() {go loop()loop()time.Sleep(time.Second) // 停頓一秒 }這次確實輸出了兩趟,目的達到了。
可是采用等待的辦法并不好,如果goroutine在結束的時候,告訴下主線說“Hey, 我要跑完了!”就好了, 即所謂阻塞主線的辦法,回憶下我們Python里面等待所有線程執(zhí)行完畢的寫法:
for thread in threads:thread.join()是的,我們也需要一個類似join的東西來阻塞住主線。那就是信道
信道
信道是什么?簡單說,是goroutine之間互相通訊的東西。類似我們Unix上的管道(可以在進程間傳遞消息), 用來goroutine之間發(fā)消息和接收消息。其實,就是在做goroutine之間的內(nèi)存共享。
使用make來建立一個信道:那如何向信道存消息和取消息呢? 一個例子:
func main() {var messages chan string = make(chan string)go func(message string) {messages <- message // 存消息}("Ping!")fmt.Println(<-messages) // 取消息 }默認的,信道的存消息和取消息都是阻塞的 (叫做無緩沖的信道,不過緩沖這個概念稍后了解,先說阻塞的問題)。
也就是說, 無緩沖的信道在取消息和存消息的時候都會掛起當前的goroutine,除非另一端已經(jīng)準備好。
比如以下的main函數(shù)和foo函數(shù):
var ch chan int = make(chan int)func foo() {ch <- 0 // 向ch中加數(shù)據(jù),如果沒有其他goroutine來取走這個數(shù)據(jù),那么掛起foo, 直到main函數(shù)把0這個數(shù)據(jù)拿走 }func main() {go foo()<- ch // 從ch取數(shù)據(jù),如果ch中還沒放數(shù)據(jù),那就掛起main線,直到foo函數(shù)中放數(shù)據(jù)為止 }- 那既然信道可以阻塞當前的goroutine, 那么回到上一部分「goroutine」所遇到的問題「如何讓goroutine告訴主線我執(zhí)行完畢了」 的問題來, 使用一個信道來告訴主線即可:
如果不用信道來阻塞主線的話,主線就會過早跑完,loop線都沒有機會執(zhí)行、、、
其實,無緩沖的信道永遠不會存儲數(shù)據(jù),只負責數(shù)據(jù)的流通,為什么這么講呢?
-
從無緩沖信道取數(shù)據(jù),必須要有數(shù)據(jù)流進來才可以,否則當前線阻塞
-
數(shù)據(jù)流入無緩沖信道, 如果沒有其他goroutine來拿走這個數(shù)據(jù),那么當前線阻塞
所以,你可以測試下,無論如何,我們測試到的無緩沖信道的大小都是0 (len(channel))
如果信道正有數(shù)據(jù)在流動,我們還要加入數(shù)據(jù),或者信道干澀,我們一直向無數(shù)據(jù)流入的空信道取數(shù)據(jù)呢? 就會引起死鎖
死鎖
一個死鎖的例子:
func main() {ch := make(chan int)<- ch // 阻塞main goroutine, 信道c被鎖 }執(zhí)行這個程序你會看到Go報這樣的錯誤:
fatal error: all goroutines are asleep - deadlock!何謂死鎖? 操作系統(tǒng)有講過的,所有的線程或進程都在等待資源的釋放。如上的程序中, 只有一個goroutine, 所以當你向里面加數(shù)據(jù)或者存數(shù)據(jù)的話,都會鎖死信道, 并且阻塞當前 goroutine, 也就是所有的goroutine(其實就main線一個)都在等待信道的開放(沒人拿走數(shù)據(jù)信道是不會開放的),也就是死鎖咯。
我發(fā)現(xiàn)死鎖是一個很有意思的話題,這里有幾個死鎖的例子:
1.只在單一的goroutine里操作無緩沖信道,一定死鎖。比如你只在main函數(shù)里操作信道:
func main() {ch := make(chan int)ch <- 1 // 1流入信道,堵塞當前線, 沒人取走數(shù)據(jù)信道不會打開fmt.Println("This line code wont run") //在此行執(zhí)行之前Go就會報死鎖 }2.如下也是一個死鎖的例子:
var ch1 chan int = make(chan int) var ch2 chan int = make(chan int)func say(s string) {fmt.Println(s)ch1 <- <- ch2 // ch1 等待 ch2流出的數(shù)據(jù) }func main() {go say("hello")<- ch1 // 堵塞主線 }其中主線等ch1中的數(shù)據(jù)流出,ch1等ch2的數(shù)據(jù)流出,但是ch2等待數(shù)據(jù)流入,兩個goroutine都在等,也就是死鎖。
其實,總結來看,為什么會死鎖?非緩沖信道上如果發(fā)生了流入無流出,或者流出無流入,也就導致了死鎖。或者這樣理解 Go啟動的所有goroutine里的非緩沖信道一定要一個線里存數(shù)據(jù),一個線里取數(shù)據(jù),要成對才行 。所以下面的示例一定死鎖:
c, quit := make(chan int), make(chan int)go func() {c <- 1 // c通道的數(shù)據(jù)沒有被其他goroutine讀取走,堵塞當前goroutinequit <- 0 // quit始終沒有辦法寫入數(shù)據(jù) }()<- quit // quit 等待數(shù)據(jù)的寫仔細分析的話,是由于:主線等待quit信道的數(shù)據(jù)流出,quit等待數(shù)據(jù)寫入,而func被c通道堵塞,所有goroutine都在等,所以死鎖。
簡單來看的話,一共兩個線,func線中流入c通道的數(shù)據(jù)并沒有在main線中流出,肯定死鎖。
但是,是否果真 所有不成對向信道存取數(shù)據(jù)的情況都是死鎖?
如下是個反例:
func main() {c := make(chan int)go func() {c <- 1}() }程序正常退出了,很簡單,并不是我們那個總結不起作用了,還是因為一個讓人很囧的原因,main又沒等待其它goroutine,自己先跑完了, 所以沒有數(shù)據(jù)流入c信道,一共執(zhí)行了一個goroutine, 并且沒有發(fā)生阻塞,所以沒有死鎖錯誤。
那么死鎖的解決辦法呢?
最簡單的,把沒取走的數(shù)據(jù)取走,沒放入的數(shù)據(jù)放入, 因為無緩沖信道不能承載數(shù)據(jù),那么就趕緊拿走!
具體來講,就死鎖例子3中的情況,可以這么避免死鎖:
c, quit := make(chan int), make(chan int)go func() {c <- 1quit <- 0 }()<- c // 取走c的數(shù)據(jù)! <-quit另一個解決辦法是緩沖信道, 即設置c有一個數(shù)據(jù)的緩沖大小:c := make(chan int, 1)這樣的話,c可以緩存一個數(shù)據(jù)。也就是說,放入一個數(shù)據(jù),c并不會掛起當前線, 再放一個才會掛起當前線直到第一個數(shù)據(jù)被其他goroutine取走, 也就是只阻塞在容量一定的時候,不達容量不阻塞。
這十分類似我們Python中的隊列Queue不是嗎?
無緩沖信道的數(shù)據(jù)進出順序
我們已經(jīng)知道,無緩沖信道從不存儲數(shù)據(jù),流入的數(shù)據(jù)必須要流出才可以。
觀察以下的程序:
var ch chan int = make(chan int)func foo(id int) { //id: 這個routine的標號ch <- id }func main() {// 開啟5個routinefor i := 0; i < 5; i++ {go foo(i)}// 取出信道中的數(shù)據(jù)for i := 0; i < 5; i++ {fmt.Print(<- ch)} }我們開了5個goroutine,然后又依次取數(shù)據(jù)。其實整個的執(zhí)行過程細分的話,5個線的數(shù)據(jù) 依次流過信道ch, main打印之, 而宏觀上我們看到的即 無緩沖信道的數(shù)據(jù)是先到先出,但是 無緩沖信道并不存儲數(shù)據(jù),只負責數(shù)據(jù)的流通
緩沖信道
終于到了這個話題了, 其實緩存信道用英文來講更為達意: buffered channel.
緩沖這個詞意思是,緩沖信道不僅可以流通數(shù)據(jù),還可以緩存數(shù)據(jù)。它是有容量的,存入一個數(shù)據(jù)的話 , 可以先放在信道里,不必阻塞當前線而等待該數(shù)據(jù)取走。
當緩沖信道達到滿的狀態(tài)的時候,就會表現(xiàn)出阻塞了,因為這時再也不能承載更多的數(shù)據(jù)了,「你們必須把 數(shù)據(jù)拿走,才可以流入數(shù)據(jù)」。
在聲明一個信道的時候,我們給make以第二個參數(shù)來指明它的容量(默認為0,即無緩沖):
var ch chan int = make(chan int, 2) // 寫入2個元素都不會阻塞當前goroutine, 存儲個數(shù)達到2的時候會阻塞如下的例子,緩沖信道ch可以無緩沖的流入3個元素:
func main() {ch := make(chan int, 3)ch <- 1ch <- 2ch <- 3 }如果你再試圖流入一個數(shù)據(jù)的話,信道ch會阻塞main線, 報死鎖。
也就是說,緩沖信道會在滿容量的時候加鎖。
其實,緩沖信道是先進先出的,我們可以把緩沖信道看作為一個線程安全的隊列:
func main() {ch := make(chan int, 3)ch <- 1ch <- 2ch <- 3fmt.Println(<-ch) // 1fmt.Println(<-ch) // 2fmt.Println(<-ch) // 3 }信道數(shù)據(jù)讀取和信道關閉
你也許發(fā)現(xiàn),上面的代碼一個一個地去讀取信道簡直太費事了,Go語言允許我們使用range來讀取信道:
func main() {ch := make(chan int, 3)ch <- 1ch <- 2ch <- 3for v := range ch {fmt.Println(v)} }如果你執(zhí)行了上面的代碼,會報死鎖錯誤的,原因是range不等到信道關閉是不會結束讀取的。也就是如果 緩沖信道干涸了,那么range就會阻塞當前goroutine, 所以死鎖咯。
那么,我們試著避免這種情況,比較容易想到的是讀到信道為空的時候就結束讀取:
ch := make(chan int, 3) ch <- 1 ch <- 2 ch <- 3 for v := range ch {fmt.Println(v)if len(ch) <= 0 { // 如果現(xiàn)有數(shù)據(jù)量為0,跳出循環(huán)break} }以上的方法是可以正常輸出的,但是注意檢查信道大小的方法不能在信道存取都在發(fā)生的時候用于取出所有數(shù)據(jù),這個例子 是因為我們只在ch中存了數(shù)據(jù),現(xiàn)在一個一個往外取,信道大小是遞減的。
另一個方式是顯式地關閉信道:
ch := make(chan int, 3) ch <- 1 ch <- 2 ch <- 3// 顯式地關閉信道 close(ch)for v := range ch {fmt.Println(v) }被關閉的信道會禁止數(shù)據(jù)流入, 是只讀的。我們?nèi)匀豢梢詮年P閉的信道中取出數(shù)據(jù),但是不能再寫入數(shù)據(jù)了。
等待多gorountine的方案
那好,我們回到最初的一個問題,使用信道堵塞主線,等待開出去的所有goroutine跑完。
這是一個模型,開出很多小goroutine, 它們各自跑各自的,最后跑完了向主線報告。
我們討論如下2個版本的方案:
只使用單個無緩沖信道阻塞主線
使用容量為goroutines數(shù)量的緩沖信道
對于方案1, 示例的代碼大概會是這個樣子:
var quit chan int // 只開一個信道func foo(id int) {fmt.Println(id)quit <- 0 // ok, finished }func main() {count := 1000quit = make(chan int) // 無緩沖for i := 0; i < count; i++ {go foo(i)}for i := 0; i < count; i++ {<- quit} }對于方案2, 把信道換成緩沖1000的:
quit = make(chan int, count) // 容量1000其實區(qū)別僅僅在于一個是緩沖的,一個是非緩沖的。
對于這個場景而言,兩者都能完成任務, 都是可以的。
-
無緩沖的信道是一批數(shù)據(jù)一個一個的「流進流出」
-
緩沖信道則是一個一個存儲,然后一起流出去
Go語言的并發(fā)和并行
不知道你有沒有注意到一個現(xiàn)象,還是這段代碼,如果我跑在兩個goroutines里面的話:
var quit chan int = make(chan int)func loop() {for i := 0; i < 10; i++ {fmt.Printf("%d ", i)}quit <- 0 }func main() {// 開兩個goroutine跑函數(shù)loop, loop函數(shù)負責打印10個數(shù)go loop()go loop()for i := 0; i < 2; i++ {<- quit} }我們觀察下輸出:
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9這是不是有什么問題??
以前我們用線程去做類似任務的時候,系統(tǒng)的線程會搶占式地輸出, 表現(xiàn)出來的是亂序地輸出。而goroutine為什么是這樣輸出的呢?
goroutine是在并行嗎?
我們找個例子測試下:
package mainimport "fmt" import "time"var quit chan intfunc foo(id int) {fmt.Println(id)time.Sleep(time.Second) // 停頓一秒quit <- 0 // 發(fā)消息:我執(zhí)行完啦! }func main() {count := 1000quit = make(chan int, count) // 緩沖1000個數(shù)據(jù)for i := 0; i < count; i++ { //開1000個goroutinego foo(i)}for i :=0 ; i < count; i++ { // 等待所有完成消息發(fā)送完畢。<- quit} }讓我們跑一下這個程序(之所以先編譯再運行,是為了讓程序跑的盡量快,測試結果更好):
go build test.go time ./test ./test 0.01s user 0.01s system 1% cpu 1.016 total我們看到,總計用時接近一秒。 貌似并行了!
我們看到,總計用時接近一秒。 貌似并行了!
并行和并發(fā)
從概念上講,并發(fā)和并行是不同的, 簡單來說看這個圖片
- 兩個隊列,一個Coffee機器,那是并發(fā)
- 兩個隊列,兩個Coffee機器,那是并行
更多的資料: 并發(fā)不是并行, 當然Google上有更多關于并行和并發(fā)的區(qū)別。
那么回到一開始的疑問上,從上面的兩個例子執(zhí)行后的表現(xiàn)來看,多個goroutine跑loop函數(shù)會挨個goroutine去進行,而sleep則是一起執(zhí)行的。
這是為什么?
默認地, Go所有的goroutines只能在一個線程里跑 。
也就是說, 以上兩個代碼都不是并行的,但是都是是并發(fā)的。
如果當前goroutine不發(fā)生阻塞,它是不會讓出CPU給其他goroutine的, 所以例子一中的輸出會是一個一個goroutine進行的,而sleep函數(shù)則阻塞掉了 當前goroutine, 當前goroutine主動讓其他goroutine執(zhí)行, 所以形成了邏輯上的并行, 也就是并發(fā)。
真正的并行
為了達到真正的并行,我們需要告訴Go我們允許同時最多使用多個核。
回到起初的例子,我們設置最大開2個原生線程, 我們需要用到runtime包(runtime包是goroutine的調(diào)度器):
import ("fmt""runtime" )var quit chan int = make(chan int)func loop() {for i := 0; i < 100; i++ { //為了觀察,跑多些fmt.Printf("%d ", i)}quit <- 0 }func main() {runtime.GOMAXPROCS(2) // 最多使用2個核go loop()go loop()for i := 0; i < 2; i++ {<- quit} }這下會看到兩個goroutine會搶占式地輸出數(shù)據(jù)了。
我們還可以這樣顯式地讓出CPU時間:
func loop() {for i := 0; i < 10; i++ {runtime.Gosched() // 顯式地讓出CPU時間給其他goroutinefmt.Printf("%d ", i)}quit <- 0 }func main() {go loop()go loop()for i := 0; i < 2; i++ {<- quit} }觀察下結果會看到這樣有規(guī)律的輸出:
0 0 1 1 2 2 3 3 4 4 5 5 6 6 7 7 8 8 9 9其實,這種主動讓出CPU時間的方式仍然是在單核里跑。但手工地切換goroutine導致了看上去的“并行”。
其實作為一個Python程序員,goroutine讓我更多地想到的是gevent的協(xié)程,而不是原生線程。
關于runtime包對goroutine的調(diào)度,在stackoverflow上有一個不錯的答案:http://stackoverflow.com/questions/13107958/what-exactly-does-runtime-gosched-do
一個小問題
我在Segmentfault看到了這個問題:?http://segmentfault.com/q/1010000000207474
題目說,如下的程序,按照理解應該打印下5次?"world"呀,可是為什么什么也沒有打印
package mainimport ("fmt" )func say(s string) {for i := 0; i < 5; i++ {fmt.Println(s)} }func main() {go say("world") //開一個新的Goroutines執(zhí)行for {} }樓下的答案已經(jīng)很棒了,這里Go仍然在使用單核,for死循環(huán)占據(jù)了單核CPU所有的資源,而main線和say兩個goroutine都在一個線程里面, 所以say沒有機會執(zhí)行。解決方案還是兩個:
- 允許Go使用多核(runtime.GOMAXPROCS)
- 手動顯式調(diào)動(runtime.Gosched)
runtime調(diào)度器
runtime調(diào)度器是個很神奇的東西,但是我真是但愿它不存在,我希望顯式調(diào)度能更為自然些,多核處理默認開啟。
關于runtime包幾個函數(shù):
- Gosched?讓出cpu
- NumCPU?返回當前系統(tǒng)的CPU核數(shù)量
- GOMAXPROCS?設置最大的可同時使用的CPU核數(shù)
- Goexit?退出當前goroutine(但是defer語句會照常執(zhí)行)
總結
我們從例子中可以看到,默認的, 所有goroutine會在一個原生線程里跑,也就是只使用了一個CPU核。
在同一個原生線程里,如果當前goroutine不發(fā)生阻塞,它是不會讓出CPU時間給其他同線程的goroutines的,這是Go運行時對goroutine的調(diào)度,我們也可以使用runtime包來手工調(diào)度。
本文開頭的兩個例子都是限制在單核CPU里執(zhí)行的,所有的goroutines跑在一個線程里面,分析如下:
- 對于代碼例子一(loop函數(shù)的那個),每個goroutine沒有發(fā)生堵塞(直到quit流入數(shù)據(jù)), 所以在quit之前每個goroutine不會主動讓出CPU,也就發(fā)生了串行打印
- 對于代碼例子二(time的那個),每個goroutine在sleep被調(diào)用的時候會阻塞,讓出CPU, 所以例子二并發(fā)執(zhí)行。
那么關于我們開啟多核的時候呢?Go語言對goroutine的調(diào)度行為又是怎么樣的?
我們可以在Golang官方網(wǎng)站的這里 找到一句話:
When a coroutine blocks, such as by calling a blocking system call, the run-time automatically moves other coroutines on the same operating system thread to a different, runnable thread so they won’t be blocked.
也就是說:
當一個goroutine發(fā)生阻塞,Go會自動地把與該goroutine處于同一系統(tǒng)線程的其他goroutines轉(zhuǎn)移到另一個系統(tǒng)線程上去,以使這些goroutines不阻塞
開啟多核的實驗
仍然需要做一個實驗,來測試下多核支持下goroutines的對原生線程的分配, 也驗證下我們所得到的結論“goroutine不阻塞不放開CPU”。
實驗代碼如下:
package mainimport ("fmt""runtime" )var quit chan int = make(chan int)func loop(id int) { // id: 該goroutine的標號for i := 0; i < 10; i++ { //打印10次該goroutine的標號fmt.Printf("%d ", id)}quit <- 0 }func main() {runtime.GOMAXPROCS(2) // 最多同時使用2個核for i := 0; i < 3; i++ { //開三個goroutinego loop(i)}for i := 0; i < 3; i++ {<- quit} }多跑幾次會看到類似這些輸出(不同機器環(huán)境不一樣):
0 0 0 0 0 1 1 0 0 1 0 0 1 0 1 2 1 2 1 2 1 2 1 2 1 2 2 2 2 2 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 2 2 0 0 0 0 0 0 0 1 1 1 1 1 0 1 0 1 0 1 2 1 2 1 2 2 2 2 2 2 2 2 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 0 2 0 2 0 2 2 2 2 2 2 2 2 0 0 0 0 0 0 0 1 0 0 1 0 1 2 1 2 1 2 1 2 1 2 1 2 1 2 1 2 2 2執(zhí)行它我們會發(fā)現(xiàn)以下現(xiàn)象:
- 有時會發(fā)生搶占式輸出(說明Go開了不止一個原生線程,達到了真正的并行)
- 有時會順序輸出, 打印完0再打印1, 再打印2(說明Go開一個原生線程,單線程上的goroutine不阻塞不松開CPU)
那么,我們還會觀察到一個現(xiàn)象,無論是搶占地輸出還是順序的輸出,都會有那么兩個數(shù)字表現(xiàn)出這樣的現(xiàn)象:
- 一個數(shù)字的所有輸出都會在另一個數(shù)字的所有輸出之前
原因是, 3個goroutine分配到至多2個線程上,就會至少兩個goroutine分配到同一個線程里,單線程里的goroutine 不阻塞不放開CPU, 也就發(fā)生了順序輸出。
Go語言并發(fā)的設計模式和應用場景
以下設計模式和應用場景來自Google IO上的關于Goroutine的PPT:https://talks.golang.org/2012/concurrency.slide
本文的示例代碼在:?https://github.com/hit9/Go-patterns-with-channel
生成器
在Python中我們可以使用yield關鍵字來讓一個函數(shù)成為生成器,在Go中我們可以使用信道來制造生成器(一種lazy load類似的東西)。
當然我們的信道并不是簡單的做阻塞主線的功能來使用的哦。
下面是一個制作自增整數(shù)生成器的例子,直到主線向信道索要數(shù)據(jù),我們才添加數(shù)據(jù)到信道
func xrange() chan int{ // xrange用來生成自增的整數(shù)var ch chan int = make(chan int)go func() { // 開出一個goroutinefor i := 0; ; i++ {ch <- i // 直到信道索要數(shù)據(jù),才把i添加進信道}}()return ch }func main() {generator := xrange()for i:=0; i < 1000; i++ { // 我們生成1000個自增的整數(shù)!fmt.Println(<-generator)} }這不禁叫我想起了Python中可愛的xrange, 所以給了生成器這個名字!
服務化
比如我們加載一個網(wǎng)站的時候,例如我們登入新浪微博,我們的消息數(shù)據(jù)應該來自一個獨立的服務,這個服務只負責 返回某個用戶的新的消息提醒。
如下是一個使用示例:
func get_notification(user string) chan string{/** 此處可以查詢數(shù)據(jù)庫獲取新消息等等..*/notifications := make(chan string)go func() { // 懸掛一個信道出去notifications <- fmt.Sprintf("Hi %s, welcome to weibo.com!", user)}()return notifications }func main() {jack := get_notification("jack") // 獲取jack的消息joe := get_notification("joe") // 獲取joe的消息// 獲取消息的返回fmt.Println(<-jack)fmt.Println(<-joe) }多路復合
上面的例子都使用一個信道作為返回值,可以把信道的數(shù)據(jù)合并到一個信道的。 不過這樣的話,我們需要按順序輸出我們的返回值(先進先出)。
如下,我們假設要計算很復雜的一個運算 100-x , 分為三路計算, 最后統(tǒng)一在一個信道中取出結果:
func do_stuff(x int) int { // 一個比較耗時的事情,比如計算time.Sleep(time.Duration(rand.Intn(10)) * time.Millisecond) //模擬計算return 100 - x // 假如100-x是一個很費時的計算 }func branch(x int) chan int{ // 每個分支開出一個goroutine做計算并把計算結果流入各自信道ch := make(chan int)go func() {ch <- do_stuff(x)}()return ch }func fanIn(chs... chan int) chan int {ch := make(chan int)for _, c := range chs {// 注意此處明確傳值go func(c chan int) {ch <- <- c}(c) // 復合}return ch }func main() {result := fanIn(branch(1), branch(2), branch(3))for i := 0; i < 3; i++ {fmt.Println(<-result)} }select監(jiān)聽信道
Go有一個語句叫做select,用于監(jiān)測各個信道的數(shù)據(jù)流動。
如下的程序是select的一個使用例子,我們監(jiān)視三個信道的數(shù)據(jù)流出并收集數(shù)據(jù)到一個信道中。有了select, 我們在 多路復合中的示例代碼中的函數(shù)fanIn還可以這么來寫(這樣就不用開好幾個goroutine來取數(shù)據(jù)了):
func fanIn(branches ... chan int) chan int {c := make(chan int)go func() {for i := 0 ; i < len(branches); i++ { //select會嘗試著依次取出各個信道的數(shù)據(jù)select {case v1 := <- branches[i]: c <- v1}}}()return c }用select的時候,有時需要超時處理, 其中的timeout信道相當有趣:結束標志
在Go并發(fā)與并行筆記一我們已經(jīng)講過信道的一個很重要也很平常的應用,就是使用無緩沖信道來阻塞主線,等待goroutine結束。
這樣我們不必再使用timeout。
那么對上面的timeout來結束主線的方案作個更新:菊花鏈
簡單地來說,數(shù)據(jù)從一端流入,從另一端流出,看上去好像一個鏈表,不知道為什么要取這么個尷尬的名字。。
菊花鏈的英文名字叫做: Daisy-chain, 它的一個應用就是做過濾器,比如我們來篩下100以內(nèi)的素數(shù)(你需要先知道什么是篩法)
程序有詳細的注釋,不再說明了。隨機數(shù)生成器
信道可以做生成器使用,作為一個特殊的例子,它還可以用作隨機數(shù)生成器。如下是一個隨機01生成器:定時器
我們剛才其實已經(jīng)接觸了信道作為定時器, time包里的After會制作一個定時器。
看看我們的定時器吧!
/** 利用信道做定時器*/package mainimport ("fmt""time" )func timer(duration time.Duration) chan bool {ch := make(chan bool)go func() {time.Sleep(duration)ch <- true // 到時間啦!}()return ch }func main() {timeout := timer(time.Second) // 定時1sfor {select {case <- timeout:fmt.Println("already 1s!") // 到時間return //結束程序}} }TODO
Google的應用場景例子。
本篇主要總結了使用信道, goroutine的一些設計模式。
?
間接轉(zhuǎn)載地址:?https://blog.csdn.net/sb___itfk/article/details/79045906
總結
- 上一篇: 栈相关经典题:每日温度
- 下一篇: Golang 新手可能会踩的 50 个坑