Go Goroutine

Go 非同步運算 (Goroutine)

Goroutine

Goroutine 是由 Go 語言本身管理的輕量級執行緒(User-space Thread),而不是由作業系統管理的執行緒(OS Thread),切換與創建成本極低。

背後使用 M:N 排程模型。這意味著,M 個 Goroutine 會被分配到 N 個作業系統執行緒上執行,當 Goroutine 阻塞時會調度繼續執行其他事情。

Kernel Space

Go Runtime

Goroutine 1

Goroutine 2

Goroutine 3

Goroutine 4

Goroutine 5

OS Thread 1

OS Thread 2

go 關鍵字用於創建 goroutine,一種 Fork-join🔗 並行計算模型實踐把大任務切成獨立的小任務同時執行並整合起來。

並行執行階段

分派任務

分派任務

分派任務

執行中

執行中

執行中

合併結果

主執行緒 Main Thread

Fork
分岔/拆分

子任務 1

子任務 2

子任務 3

Join
匯合/等待

主執行緒繼續/結束

func someFunc(num string) {
fmt.Println(num)
}
func main() {
go someFunc("1")
go someFunc("2")
go someFunc("3")
// 避免 main goroutine 執行完畢關閉,故意等待 2 秒等待其他 goroutine 執行完畢
time.Sleep(time.Second * 2)
fmt.Println("Hi")
}

可以發現每次打印數字的順序都可能不同,這就是 goroutine 背後並行執行代碼的證據。

channel

相較於其他語言共享記憶體並透過「替程式上鎖」來避免 Race Condition;Go 透過共享 Channel 確定任何給定時間只有一個 goroutine 可以存取資料。Channel 可以想像是一個 Queue:

func main() {
myChannel := make(chan string)
go func() {
myChannel <- "data"
}()
// Blocking,只有關閉或接受訊息才會繼續
msg := <-myChannel
fmt.Println(msg)
}

Unbuffered vs Buffered Channel

Channel 之間有送出與接收端,Unbuffered 意味著,只要送出 goroutine 就會一直等待直到接收:

ch := make(chan int)
go func() {
fmt.Println("sending...")
ch <- 1 // 阻塞到主 goroutine 接收
fmt.Println("sent")
}()
fmt.Println("receiving...")
value := <-ch // 進入此行,送端才會繼續
fmt.Println("received:", value)

而 Buffered 意味著送出並不一定馬上應對接收,而是送出訊息時 buffer 滿了才堵塞或是接收訊息空了才堵塞。

ch := make(chan int, 2)
ch <- 1 // 不阻塞
ch <- 2 // 不阻塞
// ch <- 3 會阻塞,因為 buffer 已滿
fmt.Println(<-ch) // 1
fmt.Println(<-ch) // 2

Select

如果多個 case 都可以執行,系統會進行統一偽隨機選擇來決定執行哪一個。

func main() {
myChannel := make(chan string)
myAnotherChannel := make(chan string)
go func() {
myChannel <- "data"
}()
go func() {
myAnotherChannel <- "data2"
}()
// Blocking,只有任一 case 關閉或接受訊息才會繼續
// 如果多個 case 都可以執行,系統會進行統一偽隨機選擇來決定執行哪一個。
select {
case msg := <-myChannel:
fmt.Println("a", msg)
case msg2 := <-myAnotherChannel:
fmt.Println("b", msg2)
}
}

延伸閱讀