無緩衝阻塞 chan 雜談

pardon110發表於2019-10-08

chan類似佇列版管道,無緩衝chan看起來好像是全域性變數,透過它可讓多個goroutine間通訊。 這其實隱含一個事實,chan阻塞會引發goroutine上下文切換,而切換到哪一個可執行goroutine由go排程器決定(與阻塞chan相關)。go當前能夠使用的goroutine,必須在其待命佇列中,否則會產生死鎖。

上下文切換

多程式多執行緒都具備上下文切換,即儲存恢復現場的能力。goroutine的上下文切換實現,是在使用者態基礎上進行,只不過它涉及到的資源比執行緒更少,如產生一個執行緒系統呼叫分配記憶體通常在1M,而goroutine只有2kb,此外在使用暫存器,段位上,goroutine也只需3個左右,而執行緒則通常在10個左右。

無緩衝阻塞

go排程器對goroutine的使用配合chan,具有有序性(在高併發訪問物件時,可用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)
 }

有緩衝通道,意味著在未超過當前通道限制數之前,當前的goroutine是非阻塞,不會發生上下文切換,即當前goroutine的控制權不發生轉移,runtime也就不會去尋求其它相關goroutine執行。

小結

  • 無緩衝chan 進和出都會阻塞.
  • 有緩衝chan 先進先出佇列, 出會一直阻塞到有資料, 進時當佇列未滿不會阻塞, 佇列已滿則阻塞.
本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章