[Golang] goroutines, channels, and concurrency
此篇為各筆記之整理,非原創內容,資料來源可見文後參考資料。
TL;DR
- 從一個 goroutine 切換到另一個 goroutine 的時機點是「當正在執行的 goroutine 阻塞時,就會交給其他 goroutine 做事」
- unbuffered channel 指的是 buffer size 為 0 的 channel
- 對於 unbuffered channel 來說,不論是從 channel 讀資料(需等到被寫入),或把資料寫入 channel 中時(需等到被讀出),都會阻塞該 goroutine
- 對於 buffered channel 來說:
- 從 channel 讀值時若是 empty buffer 時才會阻塞,否則都是 non-blocking
- 把資料寫入 channel 中時,寫入 channel 中的 value 數目(n + 1)需要超過 buffer size(n),也就是溢出(overflow)時才會使得該 goroutine 被阻塞;而且一旦 buffer channel 中的值開始被讀取,就會被全部讀完
// go routine
go f(x, y, z)
// channels
// 和 maps, slices, channels 一樣需要在使用前被建立,這裡表示定義的 chan 會回傳 int
ch := make(chan int)
ch <- v // Send v to channel ch.
v, ok := <-ch // Receive from ch, and
// assign value to v.
概念釐清
goroutines vs threads
- goroutines 是由 Go runtime 所管理的輕量化的 thread
- goroutines 會在相同的 address space 中執行,因此要存取共享的記憶體必須要是同步的(synchronized)。
- 當我們在執行 Go 程式時,Go runtime 會建立許多 threads,當某一個 goroutine 的 thread 被阻塞時,它會切換去其它 thread 執行其他的 goroutine,這個過程很類似 thread scheduling,但它是由 go runtime 來處理,而且速度更快
- 傳統的 Apache 伺服器來說,當每分鐘需要處理 1000 個請求時,每個請求如果都要 concurrently 的運作,將會需要建立 1000 個 threads 或者分派到不同的 process 去做,如果 OS 的每個 thread 都需要使用 1MB 的 stack size 的話,就會需要 1GB 的記憶體才能撐得住這樣的流量。 但相對於 goroutine 來說,因為 stack size 可以動態增長,因此可以擴充到 1000 個 goroutines,每個 goroutine 只需要 2KB(Go 1.4 之後)的 stack size。
- 在 Go 1.5 之後,Golang 預設會使用的 CPU 的數目(
GOMAXPROCS
)將會根據電腦實體 CPU 的數目來決定 - 使用越多的 CPU 來執行不見得會有更好的效能,因為不同 CPU 之間需要更多時間來進行溝通和資料交換,透過
runtime.GOMAXPROCS(n)
可以改變 go runtime 使用的處理器數目
OS thread | goroutine |
---|---|
由 OS kernel 管理,相依於硬體 | goroutines 是由 go runtime 管理,不依賴於硬體 |
OS threads 一般有固定 1-2 MB 的 stack size | goroutines 的 stack size 約 8KB(自從 Go 1.4 開始為 2KB) |
在編譯的時候就決定了 stack 的大小,並且不能增長 | 由於是在 run-time 管理 stack size,透過分配和釋放 heap storage 可以增長到 1GB |
不同 thread 之間沒有簡易的溝通媒介,並且溝通時易有延遲 | goroutine 使用 channels 來和其它的 goroutine 溝通,且低延遲 |
thread 有 identity,透過 TID 可以辨別 process 中的不同 thread | goroutine 沒有 identity |
Thread 有需要 setup 和 teardown cost,需要向 OS 請求資源並在完成時還回去 | goroutine 是在 go 的 runtime 中建立和摧毀,和 OS threads 相比非常容易,因為 go runtime 已經為 goroutines 建立了 thread pools,因此 OS 並不會留意到 coroutines |
threads 需要先被 scheduled,在不同 thread 間切換時的消耗很高,因為 scheduler 需要儲存和還原 |
資料來源:threads vs goroutines @ gist
concurrency vs parallelism
- Concurrency 指的是開啟很多的 threads 在執行程式碼,但它們並不是「同時」執行,而是透過快速切換來執行(只有一個 CPU 在負責)。
- Parallelism 指的是開啟很多 threads 「同時」執行程式碼,需要倚靠多個 CPU。
"concurrency is dealing with multiple things at once, parallelism is doing multiple things at once"(Achieving concurrency in Go)
Goroutines
- 每個 Go 程式預設都會建立一個 goroutine,這被稱作是 main goroutine,也就是函式
main
中執行的內容 - 所有的 goroutines 都是沒有名稱的(anonymous),因為 goroutine 並沒有 identity
- 在下面這段程式中,當 main goroutine 開始執行時,go 排程器(scheduler)並不會將控制權交給
printHello
這個 goroutine,因此當 goroutine 執行完畢後,程式會立即中止,而排程器並沒有機會把printHello
這個 goroutine 加入排程中。
func printHello() {
fmt.Println("Hello World")
}
func main() {
fmt.Println("main execution started")
// call function
go printHello()
fmt.Println("main execution stopped")
}
但我們知道,當 goroutine 被阻塞的時候,就會把控制權交給其他的 goroutine,因此這裡可以試著用 time.Sleep()
來把它阻塞:
func printHello() {
fmt.Println("Hello World")
}
func main() {
fmt.Println("main execution started")
// call function
go printHello()
// block here
time.Sleep(10 * time.Millisecond)
fmt.Println("main execution stopped")
}
anonymous goroutine
func main() {
fmt.Println("main() started")
c := make(chan string)
// anonymous goroutine
go func(c chan string) {
fmt.Println("Hello " + <-c + "!")
}(c)
c <- "John"
fmt.Println("main() ended")
}
Channels
var zeroC chan int // channel 的 zero value 是 nil
unbufferedC := make(chan int) // unbuffered channel 的 buffered size 是 0
bufferedC := make(chan int, 3) // capacity 為 3 的 buffered channel
Unbuffered Channels
func main() {
// channel 的 zero value 是 nil
var zeroC chan int
fmt.Println(zeroC)
// 一般建立 channel 的方式
c := make(chan int) // unbuffered channel
fmt.Printf("type of c is %T\n", c) // type of c is chan int
fmt.Printf("value of c is %v\n", c) // value of c is 0xc000062060
}
- 所有的 unbuffered channel 操作預設都是 blocking 的
- 當有資料要寫入 channel 時,goroutine 會阻塞住,直到有其他的 goroutine 從該 channel 把值讀出來
- 當有資料要讀取 channel 中的值時,goroutine 也會阻塞,直到其他 goroutine 把值寫入 channel 中
- 也就是說,當我們是的把資料寫入 channel 或從 channel 中取出資料時,該 goroutine 都會阻塞住,並且將控制權交給其他可以運行的 goroutines
// 程式來源:https://medium.com/rungo/anatomy-of-channels-in-go-concurrency-in-go-1ec336086adb
func greet(c chan string) {
fmt.Println("Hello " + <-c + "!")
}
func main() {
fmt.Println("main() started")
c := make(chan string)
go greet(c)
// block here (把控制權交給其他 goroutine,這裡也就是 greet)
c <- "John"
fmt.Println("main() stopped")
}
⚠️ 雖然讀取值的時候會 blocking 等到有值出來,但並不表示來得及被 print 出來,以下面的程式為例:
func greet(c chan string) {
fmt.Println(<-c)
fmt.Println(<-c)
}
func main() {
fmt.Println("main() started")
c := make(chan string)
go greet(c)
c <- "John"
c <- "Mike"
fmt.Println("main() stopped")
}
// main() started
// John
// main() stopped
輸出結果只會看到 John
,這是因為雖然第二個 <-c
一樣會 blocking 並等待值送入 channel,但是當 Mike 的值送入 channel 中,而 greet goroutine 收到值要 print 出來時,main goroutine 已經執行結束了,因此最終我們看不到 Mike(若想看到 Mike 可以在 main goroutine 使用 time.Sleep(time.Millisecond)
)
Close Channel(關閉頻道)
c := make(chan string)
close(c) // 關閉 channel
val, ok := c // ok 如果是 false 表示 channel 已經被關閉
- 當 channel 已經被關閉時,
ok
會是false
,value
則會是 zero value - ⚠️ 只有 sender 可以使用
close
,receiver 使用的話會發生 panic。
Deadlock
由於 channel 的資料在讀/寫時,goroutine 會阻塞,並且將控制權交給其他可以運行的 goroutines,因此若沒有其他可以運行的 goroutines 時,就會發生 deadlock
的情況,整個程式則會 crash。
也就是說,如果你試著從 channel 中讀資料,但 channel 中並沒有可以被讀取的值時,它會使得當前的 goroutine 阻塞,並期待其他 goroutine 會把值塞入這個 channel,此時「讀取資料」的操作會阻塞。相似地,如果你想要傳送資料到某一個 channel 中,它同樣會阻塞當前的 goroutine,並期待其他的 goroutine 有人去讀取這個值,這時候「寫入資料(send operation)」的操作會被阻塞。
- 從 channel 中讀不到資料 -> 讀取資料的操作會阻塞 -> deadlock
- 寫入資料到 channel -> 沒人讀取此 channel 的值 -> 寫入資料的操作會阻塞 -> deadlock
// 只是寫入資料但沒有 channel 讀取 -> deadlock
func main() {
fmt.Println("main() started")
// 只是寫入資料但沒有 channel 讀取
c := make(chan string)
c <- "John"
fmt.Println("main() stopped")
}
相似地:
// 有 goroutine 要讀取 channel 但沒 goroutine 寫入資料到 channel -> deadlock
func greet(c chan string) {
// 要讀取 channel 但沒人寫入資料
fmt.Println("Hello " + <-c + "!")
}
func main() {
fmt.Println("main() started")
c := make(chan string)
// c <- "John"
greet(c)
fmt.Println("main() stopped")
}
Buffered channel
buffered channel 寫值時,需要在 overflow 時才會 block goroutine
- unbuffered channel 指的是 buffered size 為 0 的 channel。
- unbuffered channel 不論是「從 channel 讀值」(需等到值被其他 goroutine 寫入),或「把值寫入 channel」(需等到值被其他 goroutine 讀出)都會阻塞當下的 goroutine。
- 當 buffer size 不是 0 的話,就屬於 buffered channel
- 「從 channel 讀值」時,只有在 buffered 是空的時才會 blocking
- 「把值寫入 channel」時,該 goroutine 並不會被阻塞,除非該 buffer 已經填滿(full)且溢出(overflow)。當 buffer 已經填滿(full)時,再把新的一筆資料傳入 channel 時會造成溢出(overflow),此時 goroutine 才會被阻塞。
- 讀值的動作一旦開始,就會一直到 buffer 變成 empty 為止才會結束。也就是說,讀取 channel 的那個 goroutine 需到等到 buffer 完全清空後才會阻塞。
舉例來說,在建立一個 buffered channel 後:
// 這個 chan 只能接收兩個長度的 buffer
// channel := make(chan, [type], [size])
ch := make(chan, int, 2)
- 寫值:直到該 channel 寫入到 n+1 個值以前,它都不會阻塞當前的 goroutine。
- 讀值:從該 channel 讀值時,若 buffer 是 empty 才會阻塞當前的 goroutine。
以實際的例子來說:
- 現在 buffered channel 的 size 是 3
- 在 buffered channel 中寫入 3 個值
- 由於寫入 channel 的值並沒有超出 buffered channel 的 size,因此 main goroutine 並不會被阻塞,使得 print goroutine 不會有機會取得控制權而被執行
// 程式來源:https://medium.com/rungo/anatomy-of-channels-in-go-concurrency-in-go-1ec336086adb
// 透過 squares goroutine 讀值
func print(c chan int) {
for i := 0; i <= 3; i++ {
fmt.Println(<-c)
}
}
// 在 main goroutine 寫值
func main() {
fmt.Println("main() started")
// 建立 buffered size 為 3 的 channel
c := make(chan int, 3)
go print(c)
// 寫入 3 個值
c <- 1
c <- 2
c <- 3
fmt.Println("main() close")
}
// Output:
// main() started
// main() close
但如果在 main goroutine 中多一個值寫入 channel 中(c <- 4
),此時 main goroutine 就會在這裡被 block 住:
- 在 main goroutine 中,使用了
c <- 4
後,因為超過 buffered channel 的 size,也就是溢出(overflow),因此在這裡會阻塞 - main goroutine 阻塞後,print goroutine 便有機會執行,一旦 print goroutine 開始讀取 channel 的值後,它就會把該 buffer 中的所有值都讀全部讀完
// 由於 main goroutine 被 block,print goroutine 有機會被執行
// 一旦 receiver 開始讀值,就會把所有 buffer 中的值全部讀完直到清空
func print(c chan int) {
for i := 0; i <= 3; i++ {
fmt.Println(<-c)
}
}
// 在 main goroutine 寫值
func main() {
fmt.Println("main() started")
c := make(chan int, 3)
go print(c)
c <- 1
c <- 2
c <- 3
c <- 4 // 因為超過 buffered size,這裡會 block
fmt.Println("main() close")
}
// main() started
// 1
// 2
// 3
// 4
// main() close
另一個範例:
/* Buffered Channels 即使寫值後,不用等待值被讀取,主程式就會結束 */
func main() {
c := make(chan bool, 1)
go func() {
fmt.Println("GO GO GO") // 有可能因為主程式已經執行完而看不到
// 使用 Buffered Channel 的話,不會等到 channel 中的值讀完才結束主程式
fmt.Printf("Receive value from channel %v\n", <-c)
}()
fmt.Println("Before Receive")
// STEP 1:寫入 channel
c <- true
// c <- false // block here
fmt.Println("After Receive")
}
// Before Receive
// [GO GO GO]
// [Receive value from channel true]
// After Receive
buffered channel 也有 length 和 capacity
- 和 slice 很類似,buffered channel 也有 length 和 capacity
length
指的是在 channel buffer 中還有多少數量的值還沒被讀取(queued),可以使用len(channel)
查看capacity
則是指 buffer 實際的 size,可以使用cap(channel)
查看
// 這段程式之所以不會產生 deadlock 是因為 channel 還沒有出現 overflow,所以不會 block
func main() {
c := make(chan int, 3)
c <- 1
c <- 2
fmt.Printf("Length of channel c is %v and capacity of channel c is %v \n", len(c), cap(c))
}
// Length of channel c is 2 and capacity of channel c is 3
使用 for range 可以讀取 close 後 buffered channel 中的值
func main() {
c := make(chan int, 3)
c <- 1
c <- 2
c <- 3
close(c)
for val := range c {
fmt.Println(val)
}
}
Unidirectional channels
除了可以建立可讀(read)可寫(write)的 channel 之外,還可以建立「只可讀(receive-only)」或「只可寫(send-only)」的 channel:
// 建立只可接收或只可發送的 unidirectional channels
func main() {
roc := make(<-chan int) // receive only channel
soc := make(chan<- int) // send only channel
fmt.Printf("receive only channel type is '%T' \n", roc)
fmt.Printf("send only channel type is '%T' \n", soc)
}
// receive only channel type is '<-chan int'
// send only channel type is 'chan<- int'
- 透過 unidirectional channels 可以增加型別的安全性(type-safety)
- 但如果我們希望在某一個 goroutine 中只能從 channel 讀取資料,但在 main goroutine 中可以對這個 channel 讀和寫資料時,可以透過 go 提供的語法來將 bi-directional channel 轉換成 unidirectional channel
// <-chan 表示 receive only channel type
func greet(roc <-chan string) {
fmt.Println("Hello " + <-roc + "!")
// receive only channel 不能傳送資料
// invalid operation: cannot send to receive-only type <-chan string
// roc <- "foo"
}
func main() {
fmt.Println("main() started")
c := make(chan string)
go greet(c)
c <- "John"
fmt.Println("main() stopped")
}