這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
Go語言內建了書寫並發程式的工具。將go聲明放到一個需調用的函數之前,在相同地址空間調用運行這個函數,這樣該函數執行時便會作為一個獨立的並發線程。這種線程在Go語言中稱作goroutine。在這裡我要提一下,並發並不總是意味著並行。Goroutines是指在硬體允許情況下建立能夠並存執行程式的架構。這是這個主題的一次討論:並發不是並行。
讓我們從一個例子開始:
func main() { // Start a goroutine and execute println concurrently go println("goroutine message") println("main function message")}
Garfielt 翻譯於 3年前 2人頂 頂 翻譯得不錯哦!
這段程式將輸出main function messageand 或者goroutine message。我說“ 或者”是因為催生的goroutine有一些特點。當你運行一個goroutine時,調用的代碼(在我們的例子裡它是main函數)不等待goroutine完成,而是繼續往下運行。在調用完println後main函數結束了它的執行,在Go語言裡這意味著這個程式及所有催生的goroutines停止執行。但是,在這個發生之前,goroutine可能已經完成了其代碼的執行並輸出了goroutine message字元。
你明白這些後必須有方法來避免這種情況。這就是Go語言中channels的作用。
Garfielt 翻譯於 3年前 2人頂 頂 翻譯得不錯哦!
Channels 基礎知識
Channels用來同步並發執行的函數並提供它們某種傳值交流的機制。Channels的一些特性:通過channel傳遞的元素類型、容器(或緩衝區)和傳遞的方向由“<-”操作符指定。你可以使用內建函數 make分配一個channel:
i := make(chan int) // by default the capacity is 0s := make(chan string, 3) // non-zero capacityr := make(<-chan bool) // can only read fromw := make(chan<- []os.FileInfo) // can only write to
Channels是一個第一類值(一個對象在運行期間被建立,可以當做一個參數被傳遞,從子函數返回或者可以被賦給一個變數。)可以像其他值那樣在任何地方使用:作為一個結構元素,函數參數、函數傳回值甚至另一個channel的類型:
// a channel which:// - you can only write to// - holds another channel as its valuec := make(chan<- chan bool)// function accepts a channel as a parameterfunc readFromChannel(input <-chan string) {}// function returns a channelfunc getChannel() chan bool { b := make(chan bool) return b}
cmy00cmy 翻譯於 3年前 1人頂 頂 翻譯得不錯哦! 其它翻譯版本(1)
正在載入... 在讀、寫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結束}
這個程式將順溜地列印2條資訊。為什麼呢?因為channel沒有緩衝(我們沒有指定其容量)。所有基於未緩衝的channel的的操作會將操作鎖死直到輸出和接收全部準備就緒。這就是為什麼未緩衝channel也被稱作同步(synchronous)。在我們的例子中,主函數中的操作符<-將會把程式鎖死直到goroutine在channel中寫入資料。因此程式只有在讀取操作成功結束後才會終止。 cmy00cmy 翻譯於 3年前 2人頂 頂 翻譯得不錯哦! 其它翻譯版本(1)
正在載入...
為了避免存在一個channel的緩衝區所有讀取操作都在沒有鎖定的情況下順利完成(如果緩衝區是空的)並且寫入操作也順利結束(緩衝區不滿),這樣的channel被稱作非同步的channel。下面是一個用來描述這兩者區別的例子:
func main() { message := make(chan string) // 無緩衝 count := 3 go 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) }}
在這個例子中,輸出資訊是一個同步的channel,程式輸出結果為:
send message// 等待3秒message 1send messagesend messagemessage 2message 3
正如你所看到的,在第一次goroutine中寫入channel之後,其它在同一個channel中的寫入操作都被鎖住了,直到第一次讀取操作執行完畢(大約3秒)。
現在我們提供一個緩衝區給輸出資訊的channel,例如:定義初始化行將被改為:message := make(chan string, 2)。這次程式輸出將變為:
send messagesend messagesend message// 等待3秒message 1message 2message 3
這裡我們看到所有的寫操作的執行都不會等待第一次對緩衝區的讀取操作結束,channel允許儲存所有的三條資訊。通過修改channel容器,我們通過可以控制處理資訊的總數達到限制系統輸出的目的。
cmy00cmy 翻譯於 3年前 1人頂 頂 翻譯得不錯哦!
死結
現在讓我們回到前面那個沒有成功啟動並執行讀/寫操作樣本:
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 +0x54exit status 2
這個錯誤就是我們所知的死結. 在這種情況下,兩個goroutine互相等待對方釋放資源,造成雙方都無法繼續運行。GO語言可以在運行時檢測這種死結並報錯。這個錯誤是因為鎖的自身特性產生的。
代碼在次以單線程的方式運行,逐行運行。向channel寫入的操作(c <- 42)會鎖住整個程式的執行進程,因為在同步channel中的寫操作只有在讀取器準備就緒後才能成功執行。然而在這裡,我們在寫操作的下一行才建立了讀取器。
為了使程式順利執行,我們需要做如下改動:
func main() { c := make(chan int) // 使寫操作在另一個goroutine中執行。 go func() { c <- 42 }() val := <-c println(val)}
cmy00cmy 翻譯於 3年前 2人頂 頂 翻譯得不錯哦!
範圍化的channels 和channel的關閉
在前面的一個例子中,我們向channel發送了多條資訊並讀取它們,讀取器部分的代碼如下:
for i := 1; i <= count; i++ { fmt.Println(<-message)}
為了在執行讀取操作的同時避免產生死結,我們需要知道發送訊息的確切數目,因為我們不能讀取比寫入條數還多的資料。但是這樣很不方便,下面我們就提供了一個更為人性化的方法。
在Go語言中,存在一種稱為範圍運算式的代碼,它允許程式反覆聲明數組、字串、切片、圖和channel,重複聲明會一直持續到channel的關閉。請看下面的例子(雖然現在還不能執行):
func main() { message := make(chan string) count := 3 go func() { for i := 1; i <= count; i++ { message <- fmt.Sprintf("message %d", i) } }() for msg := range message { fmt.Println(msg) }}
cmy00cmy 翻譯於 3年前 3人頂 頂 翻譯得不錯哦! 很不幸的是,這段代碼現在還不能運行。正如我們之前提到的,範圍(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)
這個特性可以被用於控制goroutine的同步,讓我們再回顧一下之前同步的例子:
func main() { done := make(chan bool) go func() { println("goroutine message") // 我們只關心被是否存在傳送這個事實,而不是值的內容。 done <- true }() println("main function message") <-done }
在這裡,done channel僅僅被用於同步程式執行,而不是發送資料。再舉一個類似的例子:
func main() { // 與資料內容無關 done := make(chan struct{}) go func() { println("goroutine message") // 發送訊號"I'm done" close(done) }() println("main function message") <-done}
我們關閉了goroutine中的channel,讀取操作不會產生鎖,因此主函數可以繼續執行下去。
cmy00cmy 翻譯於 3年前 2人頂 頂 翻譯得不錯哦!
多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) }}
這裡我們建立了一個方法,用來建立channel並定義了一個goroutine使之在一此調用中向channel發送三條資訊。我們看到,c3理應是最後一次channel調用,所以它的輸出資訊應該在其它資訊之前。但是我們得到的卻是如下輸出:
first 1second 1third 1first 2second 2third 2first 3second 3third 3
cmy00cmy 翻譯於 3年前 1人頂 頂 翻譯得不錯哦!
顯然我們成功輸出了所有的資訊,這是因為第一個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) }}
cmy00cmy 翻譯於 3年前 1人頂 頂 翻譯得不錯哦!