Golang channels 教程
Go語言內置了書寫并發程序的工具。將go聲明放到一個需調用的函數之前,在相同地址空間調用運行這個函數,這樣該函數執行時便會作為一個獨立的并發線程。這種線程在Go語言中稱作goroutine。在這里我要提一下,并發并不總是意味著并行。Goroutines是指在硬件允許情況下創建能夠并行執行程序的架構。這是這個主題的一次討論:并發不是并行。
讓我們從一個例子開始:
func main() { // Start a goroutine and execute println concurrently go println("goroutine message") println("main function message") }
這段程序將輸出main function messageand 或者goroutine message。我說“ 或者”是因為催生的goroutine有一些特點。當你運行一個goroutine時,調用的代碼(在我們的例子里它是main函數)不等待goroutine完成,而是繼續往下運行。在調用完println后main函數結束了它的執行,在Go語言里這意味著這個程序及所有催生的goroutines停止執行。但是,在這個發生之前,goroutine可能已經完成了其代碼的執行并輸出了goroutine message字符。
你明白這些后必須有方法來避免這種情況。這就是Go語言中channels的作用。
Channels 基礎知識
Channels用來同步并發執行的函數并提供它們某種傳值交流的機制。Channels的一些特性:通過channel傳遞的元素類型、容器(或緩沖區)和傳遞的方向由“<-”操作符指定。你可以使用內置函數 make分配一個channel:
i := make(chan int) // by default the capacity is 0 s := make(chan string, 3) // non-zero capacityr := make(<-chan bool) // can only read from w := make(chan<- []os.FileInfo) // can only write to</pre>Channels是一個第一類值(一個對象在運行期間被創建,可以當做一個參數被傳遞,從子函數返回或者可以被賦給一個變量。)可以像其他值那樣在任何地方使用:作為一個結構元素,函數參數、函數返回值甚至另一個channel的類型:
// a channel which: // - you can only write to // - holds another channel as its value c := make(chan<- chan bool)// function accepts a channel as a parameter func readFromChannel(input <-chan string) {}
// function returns a channel func getChannel() chan bool { b := make(chan bool) return b }</pre>在讀、寫channel的時候要格外注意 <- 操作符。它的位置關乎到channel變量的讀寫操作。下面的例子標明了它的使用方法,但我還是要提醒你,這段代碼 并不會被完整地執行,原因我們后面再講:
func main() { c := make(chan int) c <- 42 // 寫入channel val := <-c // 從channel中讀取 println(val) }現在我們知道了什么是channel,如何創建channel并且學了一些基礎操作。現在讓我們回到第一個示例,看看channel到底是如何幫助我們的。func main() { // 創建一個channel用以同步goroutine done := make(chan bool)// 在goroutine中執行輸出操作 go func() { println("goroutine message") // 告訴main函數執行完畢. // 這個channel在goroutine中是可見的 // 因為它是在相同的地址空間執行的. done <- true }() println("main function message") <-done // 等待goroutine結束
}</pre>這個程序將順溜地打印2條信息。為什么呢?因為channel沒有緩沖(我們沒有指定其容量)。所有基于未緩沖的channel的的操作會將操作鎖死直到輸出和接收全部準備就緒。這就是為什么未緩沖channel也被稱作同步(synchronous)。在我們的例子中,主函數中的操作符<-將會把程序鎖死直到goroutine在channel中寫入數據。因此程序只有在讀取操作成功結束后才會終止。
為了避免存在一個channel的緩沖區所有讀取操作都在沒有鎖定的情況下順利完成(如果緩沖區是空的)并且寫入操作也順利結束(緩沖區不滿),這樣的channel被稱作非同步的channel。下面是一個用來描述這兩者區別的例子:
func main() { message := make(chan string) // 無緩沖 count := 3go func() { for i := 1; i <= count; i++ { fmt.Println("send message") message <- fmt.Sprintf("message %d", i) } }() time.Sleep(time.Second * 3) for i := 1; i <= count; i++ { fmt.Println(<-message) }
}</pre>
在這個例子中,輸出信息是一個同步的channel,程序輸出結果為:
send message // 等待3秒 message 1 send message send message message 2 message 3正如你所看到的,在第一次goroutine中寫入channel之后,其它在同一個channel中的寫入操作都被鎖住了,直到第一次讀取操作執行完畢(大約3秒)。
現在我們提供一個緩沖區給輸出信息的channel,例如:定義初始化行將被改為:message := make(chan string, 2)。這次程序輸出將變為:
send message send message send message // 等待3秒 message 1 message 2 message 3這里我們看到所有的寫操作的執行都不會等待第一次對緩沖區的讀取操作結束,channel允許儲存所有的三條信息。通過修改channel容器,我們通過可以控制處理信息的總數達到限制系統輸出的目的。
死鎖
現在讓我們回到前面那個沒有成功運行的讀/寫操作示例:
func main() { c := make(chan int) c <- 42 // 寫入channel val := <-c // 讀取channel println(val) }一旦運行此程序,你將得到以下錯誤:fatal error: all goroutines are asleep - deadlock!goroutine 1 [chan send]: main.main() /fullpathtofile/channelsio.go:5 +0x54 exit status 2</pre>
這個錯誤就是我們所知的死鎖. 在這種情況下,兩個goroutine互相等待對方釋放資源,造成雙方都無法繼續運行。GO語言可以在運行時檢測這種死鎖并報錯。這個錯誤是因為鎖的自身特性產生的。
代碼在次以單線程的方式運行,逐行運行。向channel寫入的操作(c <- 42)會鎖住整個程序的執行進程,因為在同步channel中的寫操作只有在讀取器準備就緒后才能成功執行。然而在這里,我們在寫操作的下一行才創建了讀取器。
為了使程序順利執行,我們需要做如下改動:
func main() { c := make(chan int)// 使寫操作在另一個goroutine中執行。 go func() { c <- 42 }() val := <-c println(val)
}</pre>
范圍化的channels 和channel的關閉
在前面的一個例子中,我們向channel發送了多條信息并讀取它們,讀取器部分的代碼如下:
for i := 1; i <= count; i++ { fmt.Println(<-message) }為了在執行讀取操作的同時避免產生死鎖,我們需要知道發送消息的確切數目,因為我們不能讀取比寫入條數還多的數據。但是這樣很不方便,下面我們就提供了一個更為人性化的方法。
在Go語言中,存在一種稱為范圍表達式的代碼,它允許程序反復聲明數組、字符串、切片、圖和channel,重復聲明會一直持續到channel的關閉。請看下面的例子(雖然現在還不能執行):
func main() { message := make(chan string) count := 3go func() { for i := 1; i <= count; i++ { message <- fmt.Sprintf("message %d", i) } }() for msg := range message { fmt.Println(msg) }
}</pre>很不幸的是,這段代碼現在還不能運行。正如我們之前提到的,范圍(range)只有等到channel關閉后才會運行。因此我們需要使用 close 函數關閉channel,程序就會變成下面這個樣子:
go func() { for i := 1; i <= count; i++ { message <- fmt.Sprintf("message %d", i) } close(message) }()關閉channel還有另外一個好處——被關閉的channel內的讀取操作將不會引發鎖,而是始終長生默認的對應channel類型的值:done := make(chan bool) close(done)//不會產生鎖,打印兩次false //因為false是bool類型的默認值 println(<-done) println(<-done)</pre>這個特性可以被用于控制goroutine的同步,讓我們再回顧一下之前同步的例子:
func main() { done := make(chan bool)go func() { println("goroutine message") // 我們只關心被是否存在傳送這個事實,而不是值的內容。 done <- true }() println("main function message") <-done
} </pre>在這里,done channel僅僅被用于同步程序執行,而不是發送數據。再舉一個類似的例子:
func main() { // 與數據內容無關 done := make(chan struct{})go func() { println("goroutine message") // 發送信號"I'm done" close(done) }() println("main function message") <-done
} </pre>
我們關閉了goroutine中的channel,讀取操作不會產生鎖,因此主函數可以繼續執行下去。
多channel模式和channel的選擇
在真正的項目開發中,你可能需要多個goroutine和channel。當各部分的獨立性越強,他們之間也就越需要高效的同步措施。讓我們看個略微復雜的例子:
func getMessagesChannel(msg string, delay time.Duration) <-chan string { c := make(chan string) go func() { for i := 1; i <= 3; i++ { c <- fmt.Sprintf("%s %d", msg, i) // 在發送信息前等待 time.Sleep(time.Millisecond * delay) } }() return c }func main() { c1 := getMessagesChannel("first", 300) c2 := getMessagesChannel("second", 150) c3 := getMessagesChannel("third", 10)
for i := 1; i <= 3; i++ { println(<-c1) println(<-c2) println(<-c3) }
}</pre>這里我們創建了一個方法,用來創建channel并定義了一個goroutine使之在一此調用中向channel發送三條信息。我們看到,c3理應是最后一次channel調用,所以它的輸出信息應該在其它信息之前。但是我們得到的卻是如下輸出:
first 1 second 1 third 1 first 2 second 2 third 2 first 3 second 3 third 3顯然我們成功輸出了所有的信息,這是因為第一個channel中的讀取操作在每個循環聲明中被鎖住300毫秒,其它操作必須隨之進入等待狀態。而我們期望的卻是從所有channel中盡快讀取信息。
我們可以使用select 在多個channel之間進行選擇。這種選擇類似于普通的switch,但是所有的情況在這里都是數值傳遞操作(讀/寫)。即使操作數增加,程序也不會在更多的鎖下運行。因此,如果想要達到我們之前的目的,我們可以這么改寫程序:
for i := 1; i <= 9; i++ { select { case msg := <-c1: println(msg) case msg := <-c2: println(msg) case msg := <-c3: println(msg) } }注意循環中的9這個數:每個channel存在三個寫操作,這就是為什么這里需要9次循環的原因。在一般的守護進程中,我們可以使用無限循環執行選擇操作,但如果我在這里那么做了,那我們將得到一個死鎖:
first 1 second 1 third 1 // 這個channel將不會等待其他channel third 2 third 3 second 2 first 2 second 3 first 3總結.
channel是Go語言中頗為有趣的一個機制。但是在高效地使用它們之前你必須搞清楚他們是如何工作的。我試圖在本文中對channel做出最基礎的解釋,如果你想要更深入地學習這個機制,我建議你閱讀以下文章:
- 并發不是并行 - Rob Pike 之前我們有提到過
- Go語言并發模式——初級篇
- Go語言并發模式——高級篇 </ul>