如果不是我對真正並行的線程的追求,就不會認識到Go有多么的迷人。
Go語言從語言層面上就支持了並發,這與其他語言大不一樣,不像以前我們要用Thread庫 來新建線程,還要用線程安全的隊列庫來共享數據。
以下是我入門的學習筆記。
Go語言的goroutines、信道和死鎖
goroutine
Go語言中有個概念叫做goroutine, 這類似我們熟知的線程,但是更輕。
以下的程序,我們串行地去執行兩次loop
函數:
func loop() { for i := 0; i < 10; i++ { fmt.Printf("%d ", i) } } func main() { loop() loop() }
毫無疑問,輸出會是這樣的:
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() // 啟動一個goroutine loop() }
這次的輸出變成了:
0 1 2 3 4 5 6 7 8 9
可是為什么只輸出了一趟呢?明明我們主線跑了一趟,也開了一個goroutine來跑一趟啊。
原來,在goroutine還沒來得及跑loop的時候,主函數已經退出了。
main函數退出地太快了,我們要想辦法阻止它過早地退出,一個辦法是讓main等待一下:
func main() { go loop() loop() time.Sleep(time.Second) // 停頓一秒 }
這次確實輸出了兩趟,目的達到了。
可是采用等待的辦法並不好,如果goroutine在結束的時候,告訴下主線說“Hey, 我要跑完了!”就好了, 即所謂阻塞主線的辦法,回憶下我們Python里面等待所有線程執行完畢的寫法:
for thread in threads: thread.join()
是的,我們也需要一個類似join
的東西來阻塞住主線。那就是信道
信道
信道是什么?簡單說,是goroutine之間互相通訊的東西。類似我們Unix上的管道(可以在進程間傳遞消息), 用來goroutine之間發消息和接收消息。其實,就是在做goroutine之間的內存共享。
使用make
來建立一個信道:
var channel chan int = make(chan int) // 或 channel := make(chan int)
那如何向信道存消息和取消息呢? 一個例子:
func main() { var messages chan string = make(chan string) go func(message string) { messages <- message // 存消息 }("Ping!") fmt.Println(<-messages) // 取消息 }
默認的,信道的存消息和取消息都是阻塞的 (叫做無緩沖的信道,不過緩沖這個概念稍后了解,先說阻塞的問題)。
也就是說, 無緩沖的信道在取消息和存消息的時候都會掛起當前的goroutine,除非另一端已經准備好。
比如以下的main函數和foo函數:
var ch chan int = make(chan int) func foo() { ch <- 0 // 向ch中加數據,如果沒有其他goroutine來取走這個數據,那么掛起foo, 直到main函數把0這個數據拿走 } func main() { go foo() <- ch // 從ch取數據,如果ch中還沒放數據,那就掛起main線,直到foo函數中放數據為止 }
那既然信道可以阻塞當前的goroutine, 那么回到上一部分「goroutine」所遇到的問題「如何讓goroutine告訴主線我執行完畢了」 的問題來, 使用一個信道來告訴主線即可:
var complete chan int = make(chan int) func loop() { for i := 0; i < 10; i++ { fmt.Printf("%d ", i) } complete <- 0 // 執行完畢了,發個消息 } func main() { go loop() <- complete // 直到線程跑完, 取到消息. main在此阻塞住 }
如果不用信道來阻塞主線的話,主線就會過早跑完,loop線都沒有機會執行、、、
其實,無緩沖的信道永遠不會存儲數據,只負責數據的流通,為什么這么講呢?
-
從無緩沖信道取數據,必須要有數據流進來才可以,否則當前線阻塞
-
數據流入無緩沖信道, 如果沒有其他goroutine來拿走這個數據,那么當前線阻塞
所以,你可以測試下,無論如何,我們測試到的無緩沖信道的大小都是0 (len(channel)
)
如果信道正有數據在流動,我們還要加入數據,或者信道干澀,我們一直向無數據流入的空信道取數據呢? 就會引起死鎖
死鎖
一個死鎖的例子:
func main() { ch := make(chan int) <- ch // 阻塞main goroutine, 信道c被鎖 }
執行這個程序你會看到Go報這樣的錯誤:
fatal error: all goroutines are asleep - deadlock!
何謂死鎖? 操作系統有講過的,所有的線程或進程都在等待資源的釋放。如上的程序中, 只有一個goroutine, 所以當你向里面加數據或者存數據的話,都會鎖死信道, 並且阻塞當前 goroutine, 也就是所有的goroutine(其實就main線一個)都在等待信道的開放(沒人拿走數據信道是不會開放的),也就是死鎖咯。
我發現死鎖是一個很有意思的話題,這里有幾個死鎖的例子:
-
只在單一的goroutine里操作無緩沖信道,一定死鎖。比如你只在main函數里操作信道:
func main() { ch := make(chan int) ch <- 1 // 1流入信道,堵塞當前線, 沒人取走數據信道不會打開 fmt.Println("This line code wont run") //在此行執行之前Go就會報死鎖 }
-
如下也是一個死鎖的例子:
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流出的數據 } func main() { go say("hello") <- ch1 // 堵塞主線 }
其中主線等ch1中的數據流出,ch1等ch2的數據流出,但是ch2等待數據流入,兩個goroutine都在等,也就是死鎖。
-
其實,總結來看,為什么會死鎖?非緩沖信道上如果發生了流入無流出,或者流出無流入,也就導致了死鎖。或者這樣理解 Go啟動的所有goroutine里的非緩沖信道一定要一個線里存數據,一個線里取數據,要成對才行 。所以下面的示例一定死鎖:
c, quit := make(chan int), make(chan int) go func() { c <- 1 // c通道的數據沒有被其他goroutine讀取走,堵塞當前goroutine quit <- 0 // quit始終沒有辦法寫入數據 }() <- quit // quit 等待數據的寫
仔細分析的話,是由於:主線等待quit信道的數據流出,quit等待數據寫入,而func被c通道堵塞,所有goroutine都在等,所以死鎖。
簡單來看的話,一共兩個線,func線中流入c通道的數據並沒有在main線中流出,肯定死鎖。
但是,是否果真 所有不成對向信道存取數據的情況都是死鎖?
如下是個反例:
func main() { c := make(chan int) go func() { c <- 1 }() }
程序正常退出了,很簡單,並不是我們那個總結不起作用了,還是因為一個讓人很囧的原因,main又沒等待其它goroutine,自己先跑完了, 所以沒有數據流入c信道,一共執行了一個goroutine, 並且沒有發生阻塞,所以沒有死鎖錯誤。
那么死鎖的解決辦法呢?
最簡單的,把沒取走的數據取走,沒放入的數據放入, 因為無緩沖信道不能承載數據,那么就趕緊拿走!
具體來講,就死鎖例子3中的情況,可以這么避免死鎖:
c, quit := make(chan int), make(chan int) go func() { c <- 1 quit <- 0 }() <- c // 取走c的數據! <-quit
另一個解決辦法是緩沖信道, 即設置c有一個數據的緩沖大小:
c := make(chan int, 1)
這樣的話,c可以緩存一個數據。也就是說,放入一個數據,c並不會掛起當前線, 再放一個才會掛起當前線直到第一個數據被其他goroutine取走, 也就是只阻塞在容量一定的時候,不達容量不阻塞。
這十分類似我們Python中的隊列Queue
不是嗎?
無緩沖信道的數據進出順序
我們已經知道,無緩沖信道從不存儲數據,流入的數據必須要流出才可以。
觀察以下的程序:
var ch chan int = make(chan int) func foo(id int) { //id: 這個routine的標號 ch <- id } func main() { // 開啟5個routine for i := 0; i < 5; i++ { go foo(i) } // 取出信道中的數據 for i := 0; i < 5; i++ { fmt.Print(<- ch) } }
我們開了5個goroutine,然后又依次取數據。其實整個的執行過程細分的話,5個線的數據 依次流過信道ch, main打印之, 而宏觀上我們看到的即 無緩沖信道的數據是先到先出,但是 無緩沖信道並不存儲數據,只負責數據的流通
緩沖信道
終於到了這個話題了, 其實緩存信道用英文來講更為達意: buffered channel.
緩沖這個詞意思是,緩沖信道不僅可以流通數據,還可以緩存數據。它是有容量的,存入一個數據的話 , 可以先放在信道里,不必阻塞當前線而等待該數據取走。
當緩沖信道達到滿的狀態的時候,就會表現出阻塞了,因為這時再也不能承載更多的數據了,「你們必須把 數據拿走,才可以流入數據」。
在聲明一個信道的時候,我們給make以第二個參數來指明它的容量(默認為0,即無緩沖):
var ch chan int = make(chan int, 2) // 寫入2個元素都不會阻塞當前goroutine, 存儲個數達到2的時候會阻塞
如下的例子,緩沖信道ch可以無緩沖的流入3個元素:
func main() { ch := make(chan int, 3) ch <- 1 ch <- 2 ch <- 3 }
如果你再試圖流入一個數據的話,信道ch會阻塞main線, 報死鎖。
也就是說,緩沖信道會在滿容量的時候加鎖。
其實,緩沖信道是先進先出的,我們可以把緩沖信道看作為一個線程安全的隊列:
func main() { ch := make(chan int, 3) ch <- 1 ch <- 2 ch <- 3 fmt.Println(<-ch) // 1 fmt.Println(<-ch) // 2 fmt.Println(<-ch) // 3 }
信道數據讀取和信道關閉
你也許發現,上面的代碼一個一個地去讀取信道簡直太費事了,Go語言允許我們使用range
來讀取信道:
func main() { ch := make(chan int, 3) ch <- 1 ch <- 2 ch <- 3 for v := range ch { fmt.Println(v) } }
如果你執行了上面的代碼,會報死鎖錯誤的,原因是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 { // 如果現有數據量為0,跳出循環 break } }
以上的方法是可以正常輸出的,但是注意檢查信道大小的方法不能在信道存取都在發生的時候用於取出所有數據,這個例子 是因為我們只在ch中存了數據,現在一個一個往外取,信道大小是遞減的。
另一個方式是顯式地關閉信道:
ch := make(chan int, 3) ch <- 1 ch <- 2 ch <- 3 // 顯式地關閉信道 close(ch) for v := range ch { fmt.Println(v) }
被關閉的信道會禁止數據流入, 是只讀的。我們仍然可以從關閉的信道中取出數據,但是不能再寫入數據了。
等待多gorountine的方案
那好,我們回到最初的一個問題,使用信道堵塞主線,等待開出去的所有goroutine跑完。
這是一個模型,開出很多小goroutine, 它們各自跑各自的,最后跑完了向主線報告。
我們討論如下2個版本的方案:
-
只使用單個無緩沖信道阻塞主線
-
使用容量為goroutines數量的緩沖信道
對於方案1, 示例的代碼大概會是這個樣子:
var quit chan int // 只開一個信道 func foo(id int) { fmt.Println(id) quit <- 0 // ok, finished } func main() { count := 1000 quit = 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
其實區別僅僅在於一個是緩沖的,一個是非緩沖的。
對於這個場景而言,兩者都能完成任務, 都是可以的。
-
無緩沖的信道是一批數據一個一個的「流進流出」
- 緩沖信道則是一個一個存儲,然后一起流出去
原文引用:https://blog.csdn.net/smilesundream/article/details/80209026