21.go協程(Goroutine)
歡迎來到Golang 系列教程的第 21 章。
在前面的教程裡,我們探討了併發,以及併發與並行的區別。本教程則會介紹在 Go 語言裡,如何使用 Go 協程(Goroutine)來實現併發。
Go 協程是什麼?
Go 協程是與其他函式或方法一起併發執行的函式或方法。Go 協程可以看作是輕量級執行緒。與執行緒相比,建立一個 Go 協程的成本很小。因此在 Go 應用中,常常會看到有數以千計的 Go 協程併發地執行。
Go 協程相比於執行緒的優勢
- 相比執行緒而言,Go 協程的成本極低。堆疊大小隻有若干 kb,並且可以根據應用的需求進行增減。而執行緒必須指定堆疊的大小,其堆疊是固定不變的。
- Go 協程會複用(Multiplex)數量更少的 OS 執行緒。即使程式有數以千計的 Go 協程,也可能只有一個執行緒。如果該執行緒中的某一 Go 協程發生了阻塞(比如說等待使用者輸入),那麼系統會再建立一個 OS 執行緒,並把其餘 Go 協程都移動到這個新的 OS 執行緒。所有這一切都在執行時進行,作為程式設計師,我們沒有直接面臨這些複雜的細節,而是有一個簡潔的 API 來處理併發。
- Go 協程使用通道(Channel)來進行通訊。通道用於防止多個協程訪問共享記憶體時發生競態條件(Race Condition)。通道可以看作是 Go 協程之間通訊的管道。我們會在下一教程詳細討論通道。
如何啟動一個 Go 協程?
呼叫函式或者方法時,在前面加上關鍵字 go
,可以讓一個新的 Go 協程併發地執行。
讓我們建立一個 Go 協程吧。
package main
import (
"fmt"
)
func hello() {
fmt.Println("Hello world goroutine")
}
func main() {
go hello()
fmt.Println("main function")
}
在第 11 行,go hello()
啟動了一個新的 Go 協程。現在 hello()
函式與 main()
函式會併發地執行。主函式會執行在一個特有的 Go 協程上,它稱為 Go 主協程(Main Goroutine)。
執行一下程式,你會很驚訝!
該程式只會輸出文字 main function
。我們啟動的 Go 協程究竟出現了什麼問題?要理解這一切,我們需要理解兩個 Go 協程的主要性質。
- 啟動一個新的協程時,協程的呼叫會立即返回。與函式不同,程式控制不會去等待 Go 協程執行完畢。在呼叫 Go 協程之後,程式控制會立即返回到程式碼的下一行,忽略該協程的任何返回值。
- 如果希望執行其他 Go 協程,Go 主協程必須繼續執行著。如果 Go 主協程終止,則程式終止,於是其他 Go 協程也不會繼續執行。
現在你應該能夠理解,為何我們的 Go 協程沒有運行了吧。在第 11 行呼叫了 go hello()
之後,程式控制沒有等待 hello
協程結束,立即返回到了程式碼下一行,列印 main function
。接著由於沒有其他可執行的程式碼,Go 主協程終止,於是 hello
協程就沒有機會運行了。
我們現在修復這個問題。
package main
import (
"fmt"
"time"
)
func hello() {
fmt.Println("Hello world goroutine")
}
func main() {
go hello()
time.Sleep(1 * time.Second)
fmt.Println("main function")
}
在上面程式的第 13 行,我們呼叫了 time 包裡的函式 Sleep
,該函式會休眠執行它的 Go 協程。在這裡,我們使 Go 主協程休眠了 1 秒。因此在主協程終止之前,呼叫 go hello()
就有足夠的時間來執行了。該程式首先列印 Hello world goroutine
,等待 1 秒鐘之後,接著列印 main function
。
在 Go 主協程中使用休眠,以便等待其他協程執行完畢,這種方法只是用於理解 Go 協程如何工作的技巧。通道可用於在其他協程結束執行之前,阻塞 Go 主協程。我們會在下一教程中討論通道。
啟動多個 Go 協程
為了更好地理解 Go 協程,我們再編寫一個程式,啟動多個 Go 協程。
package main
import (
"fmt"
"time"
)
func numbers() {
for i := 1; i <= 5; i++ {
time.Sleep(250 * time.Millisecond)
fmt.Printf("%d ", i)
}
}
func alphabets() {
for i := 'a'; i <= 'e'; i++ {
time.Sleep(400 * time.Millisecond)
fmt.Printf("%c ", i)
}
}
func main() {
go numbers()
go alphabets()
time.Sleep(3000 * time.Millisecond)
fmt.Println("main terminated")
}
在上面程式中的第 21 行和第 22 行,啟動了兩個 Go 協程。現在,這兩個協程併發地執行。numbers
協程首先休眠 250 微秒,接著列印 1
,然後再次休眠,列印 2
,依此類推,一直到列印 5
結束。alphabete
協程同樣列印從 a
到 e
的字母,並且每次有 400 微秒的休眠時間。 Go 主協程啟動了 numbers
和 alphabete
兩個 Go 協程,休眠了 3000 微秒後終止程式。
該程式會輸出:
1 a 2 3 b 4 c 5 d e main terminated
程式的運作如下圖所示。為了更好地觀看圖片,請在新標籤頁中開啟。
第一張藍色的圖表示 numbers
協程,第二張褐紅色的圖表示 alphabets
協程,第三張綠色的圖表示 Go 主協程,而最後一張黑色的圖把以上三種協程合併了,表明程式是如何執行的。在每個方框頂部,諸如 0 ms
和 250 ms
這樣的字串表示時間(以微秒為單位)。在每個方框的底部,1
、2
、3
等表示輸出。藍色方框表示:250 ms
打印出 1
,500 ms
打印出 2
,依此類推。最後黑色方框的底部的值會是 1 a 2 3 b 4 c 5 d e main terminated
,這同樣也是整個程式的輸出。以上圖片非常直觀,你可以用它來理解程式是如何運作的。
Go 協程的介紹到此結束。祝你愉快。