這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
上一篇主要討論了無緩衝通道遭遇死結的幾種情況,這篇文章我們繼續討論通道的另一種類型——緩衝通道(buffered channel)。
基本性質
緩衝通道顧名思義,就是帶有緩衝區(buffered)的通道。緩衝區作為資料的臨時儲存地區,可以作為資料的臨時存放空間。初始化如下:
var ch = make(chan int, 1)
make的第二個參數代表緩衝區的長度,也就是說,通道ch在接收到第一個訊息的時候不會掛起,它會把訊息存到緩衝區中等待接收的goroutine將其提走。如果此時未提走而新的訊息到達,通道將會阻塞並掛起。
更詳細地舉個例子:
func main() { ch := make(chan int, 3) ch <- 1 ch <- 2 ch <- 3}
在這段代碼中,通道ch可以緩衝三個資料,在流入一個資料main函數將掛起並返回死結錯誤。
緩衝通道和無緩衝通道的一個區別在於,在沒有滿容量的時候,緩衝通道可以在同一個goroutine中完成資料的傳輸和提取:
import "fmt"func main() { ch := make(chan int, 3) ch <- 1 ch <- 2 ch <- 3 fmt.Println(<-ch) fmt.Println(<-ch) fmt.Println(<-ch)}
結合前文提到通道的隊列屬性,這帶來的好處是,在未滿容量的情況下,緩衝通道可以作為安全執行緒的隊列使用。
通道訊息的讀模數式
接收通道資料的方式除了一個一個讀取(如上範例程式碼)之外,Go還提供了range關鍵字:
import "fmt"func main() { ch := make(chan int, 3) ch <- 1 ch <- 2 ch <- 3 // close(ch) for value := range ch { fmt.Println(value) }}
然而上述代碼在執行完畢後會報deadlock的錯誤,其原因在於range不會自動檢測通道是否乾涸(drained),在提取完全部資料後,再次提取會使main函數掛起。解決方案也有兩個,第一個是在for迴圈中設定長度檢測,如果通道buffer為空白則跳出迴圈;第二種是在接受完全部資料後關閉通道。這裡值得注意的一點是,關閉狀態的通道永遠不會阻塞。
第二種方法揭示了通道的另一個特性:對於關閉的通道無法再接收新的資料,但是可以嘗試提取其中存留的資料。
容量為1的緩衝通道
根據通道的特性我們不難發現,如果將通道容量設定為1,我們可以利用它作為訊號量(semaphore)來保護共用變數(shared variable)的安全執行緒。
// gop1.io/ch9/bank2var ( sema = make(chan struct{}, 1) balance int)func Deposit(amount int) { sema <- struct{} // acquire token balance = balance + amount <- sema // release token}func Balance() int { sema <- struct{} // acquire token b := balance <-sema // release token return b}
這種為共用變數加的鎖我們稱之為二元訊號量(binary semaphore)。由於二元訊號量十分有用,Go甚至提供了專門的sync庫協助我們更方便地使用鎖。上述代碼可以改寫如下:
import "sync"var ( mu sync.Mutex // guards balance balance int)func Deposit(amount int) { mu.Lock() balance = balance + amount mu.Unlock()}func Balance() int { mu.Lock() defer mu.Unlock() // maybe more operations here return balance}
Balance函數中我們利用defer防止因error或者panic導致鎖未解除。
Reference: Donovan, Alan AA, and Brian W. Kernighan. The Go programming language. Addison-Wesley Professional, 2015.
-完-