chan類似隊列版管道,無緩衝chan看起來好像是全局變量,通過它可讓多個goroutine間通信。 這其實隱含一個事實,chan阻塞會引發goroutine上下文切換,而切換到哪一個可執行goroutine由go調度器決定(與阻塞chan相關)。go當前能夠使用的goroutine,必須在其待命隊列中,否則會產生死鎖。
上下文切換
多進程多線程都具備上下文切換,即保存恢復現場的能力。goroutine的上下文切換實現,是在用戶態基礎上進行,只不過它涉及到的資源比線程更少,如產生一個線程系統調用分配內存通常在1到8M,而goroutine只有4k,此外在使用寄存器,段位上,goroutine也只需3個左右,而線程則通常在10個左右。
無緩衝阻塞
go調度器對goroutine的使用配合chan,具有有序性。main函數是特殊的入口goroutine,若有阻塞代碼,運行時runtime會尋找已入隊列的goroutine並在適當的時機調用它。chan並不是全局變量,確切來說它的讀/寫阻塞會觸發當前goroutine執行權轉移,它只是個通信器。好似打電話,必須先知道對方號碼並有連線,才能正常工作,若順序不對,表現在golang中便是死鎖
Blocking
package main
import (
"fmt"
)
func f1(in chan int) {
fmt.Println(<-in)
}
func main() {
out := make(chan int)
out <- 2
go f1(out)
}
上述代碼會產生死鎖,main入口goroutine,通道out產生了發送阻塞,此時runtime會嘗試調度與out通道讀相關的goroutine執行,但可惜的是,在 out <- 2
之前,並沒有向go執行器隊列加入與out讀相關的goroutine。換句話而言,f1壓根就沒入隊,沒有執行機會。
unblocking
package main
import "fmt"
func main() {
out := make(chan int)
go f1(out)
// 此處順序大有講究,在使用發送通道之前必需想好數據接收的退路,f1即是
out <- 2
}
func f1(in chan int) {
fmt.Println(<-in)
}
chan vs 全局變量
上文提到chan類似管道,管道顧名思義一端進一端出,很形象表明了一個連接器。go中的chan連接goroutine,遊離於衆多goroutine之間,功用性與全局變量有得一拼。但chan絕對不是全局變量,一個全局變量,可以在同一函數體內重複讀寫,但對無緩衝chan而言是不可以,原因在同一goroutine內對同一chan讀寫時,存在讀或寫阻塞面臨切換上下文,另一個對應的永遠沒執行機會,如下
- 無緩衝通道死鎖
package main
import "fmt"
func main() {
ch := make(chan int)
ch <- 5
fmt.Println(<-ch)
}
- 有緩衝通道正常
package main
import "fmt"
func main() {
ch := make(chan int, 1)
ch <- 5
fmt.Println(<-ch)
}