golang 學習 ---- channel
把一個loop放在一個goroutine裡跑,我們可以使用關鍵字go
來定義並啟動一個goroutine:
package main import "fmt" func loop() { for i := 0; i < 10; i++ { fmt.Printf("%d ", i) } } func main() { go loop() // 啟動一個goroutine loop() }
輸出:
0 1 2 3 4 5 6 7 8 9
可是為什麼只輸出了一趟呢?明明我們主線跑了一趟,也開了一個goroutine來跑一趟啊。
原來,在goroutine還沒來得及跑loop的時候,主函式已經退出了。
main函式退出地太快了,我們要想辦法阻止它過早地退出,一個辦法是讓main等待一下:
package main import ( "fmt" "time" ) func loop() { for i := 0; i < 10; i++ { fmt.Printf("%d ", i) } } func main() { go loop() // 啟動一個goroutine loop() time.Sleep(time.Second) }
可是採用等待的辦法並不好,如果goroutine在結束的時候,告訴下主線說“Hey, 我要跑完了!”就好了, 即所謂阻塞主線的辦法,回憶下我們
for thread in threads: thread.join()
是的,我們也需要一個類似join
的東西來阻塞住主線。那就是通道(channel)
channel是goroutine之間互相通訊的東西。類似我們Unix上的管道(可以在程序間傳遞訊息), 用來goroutine之間發訊息和接收訊息。其實,就是在做goroutine之間的記憶體共享。
使用make
來建立一個通道:
var channel chan int = make(chan int) // 或 channel := make(chan int)
那如何向通道存訊息和取訊息呢? 一個例子:
package main import ( "fmt" ) func main() { var msg chan string = make(chan string)//無緩衝channel go func(message string) { msg <- message // 存訊息 }("Ping!") fmt.Println(<-msg) // 取訊息 }
預設的,通道的存訊息和取訊息都是阻塞的 (叫做無緩衝的通道,不過緩衝這個概念稍後瞭解,先說阻塞的問題)。
也就是說, 無緩衝的通道在取訊息和存訊息的時候都會掛起當前的goroutine,除非另一端已經準備好。
比如以下的main函式和foo函式:
package main 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告訴主線我執行完畢了」 的問題來, 使用一個通道來告訴主線即可:
package main import "fmt" 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)
)
如果通道正有資料在流動,我們還要加入資料,或者通道乾澀,我們一直向無資料流入的空通道取資料呢? 就會引起死鎖
死鎖
一個死鎖的例子:
package main func main() { ch := make(chan int) <-ch // 阻塞main goroutine, 通道c被鎖 }
執行這個程式你會看到Go報這樣的錯誤:
fatal error: all goroutines are asleep - deadlock!
何謂死鎖? 作業系統有講過的,所有的執行緒或程序都在等待資源的釋放。如上的程式中, 只有一個goroutine, 所以當你向裡面加資料或者存資料的話,都會鎖死通道, 並且阻塞當前 goroutine, 也就是所有的goroutine(其實就main線一個)都在等待通道的開放(沒人拿走資料通道是不會開放的),也就是死鎖咯。
我發現死鎖是一個很有意思的話題,這裡有幾個死鎖的例子:
只在單一的goroutine裡操作無緩衝通道,一定死鎖。比如你只在main函式裡操作通道:
package main import "fmt" func main() { ch := make(chan int) ch <- 1 // 1流入通道,堵塞當前線, 沒人取走資料通道不會開啟 fmt.Println("This line code wont run") //在此行執行之前Go就會報死鎖 }
主線等ch1中的資料流出,ch1等ch2的資料流出,但是ch2等待資料流入,兩個goroutine都在等,也就是死鎖
package main import "fmt" 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 // 堵塞主線 }
總結來看,為什麼會死鎖?非緩衝通道上如果發生了流入無流出,或者流出無流入,也就導致了死鎖。或者這樣理解 Go啟動的所有goroutine裡的非緩衝通道一定要一個線裡存資料,一個線裡取資料,要成對才行 。所以下面的示例一定死鎖:
package main func main() { 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都在等,所以死鎖。
修正死鎖
package main func main() { c, quit := make(chan int), make(chan int) go func() { c <- 1 // c通道的資料沒有被其他goroutine讀取走,堵塞當前goroutine quit <- 0 // quit始終沒有辦法寫入資料 }() go func() { <-c <-quit }() }
給channel增加緩衝區,然後在程式的最後讓主執行緒休眠一秒,程式碼如下:
package main import ( "fmt" "time" ) func main() { ch := make(chan int, 1) ch <- 1 go func() { v := <-ch fmt.Println(v) }() time.Sleep(1 * time.Second) fmt.Println("2") }