管道(Channel)
概念
Go在語言層提供的協程間通訊的方式
初始化
var ch chan int // 宣告管道
// 值為 nil
ch1 := make(chan int) // 無緩衝管道
ch2 := make(chan int, 2) // 帶緩衝管道
操作
操作符
操作符 “<-“ 表示資料流向,在函式間傳遞時可以用操作符限制管道的讀寫
ch <- 1 // 向管道寫入資料
<- ch // 從管道中讀取資料
close(ch) // 關閉管道
// 嘗試向關閉的管道寫入資料會觸發 panic
ch <- 2
// 但關閉的管道仍可讀
// 第一個變數表示讀出的資料,第二個變數(bool 型別)表示是否成功讀取了資料,需要注意的是,第二個變數不用於知識管道的關閉狀態
v, ok := <- ch
func ChanParamRW(ch chan int) {
// 可讀可寫管道
}
func ChanParamR(ch <-chan int) {
// 只讀管道
}
func chanParamW(ch chan<- int) {
// 只寫管道
}
資料讀寫
管道沒有緩衝區,讀寫資料會阻塞,直到有協程向管道中寫讀資料。
有緩衝區但沒有緩衝資料時讀操作也會阻塞協程直到有資料寫入才會喚醒該阻塞協程,向管道寫入資料,緩衝區滿了也會阻塞,直到有協程從緩衝區讀取資料。
值為 nil 的管道無論讀寫都會阻塞,而且是永久阻塞。
使用 select 可以監控多個管道,當其中某一個管道可操作時就觸發響應的 case 分支。事實上 select 語句的多個 case 語句的執行順序是隨機的。
其他操作
內建函式 len()
和 cap()
作用於管道,分別用於查詢緩衝區中資料的個數以及緩衝區的大小。
管道實現了一種 FIFO(先入先出)的佇列,資料總是按照寫入的順序流出管道。
協程讀取管道時阻塞的條件有:
- 管道無緩衝區
- 管道的緩衝區中無資料
- 管道的值為 nil
協程寫入管道時阻塞的條件有:
- 管道無緩衝區
- 管道的緩衝區已滿
- 管道的值為 nil
實現原理
資料結構
src/runtime/chann.go: hchan
32 type hchan struct {
33 qcount uint // 當前佇列中的剩餘元素
34 dataqsiz uint // 環形佇列長度,即可以存放的元素個數
35 buf unsafe.Pointer // 環形佇列指標
36 elemsize uint16 // 每個元素的大小
37 closed uint32 // 標識關閉狀態
38 elemtype *_type // 元素型別
39 sendx uint // send index 佇列下標,指示元素寫入時存放到佇列中的位置
40 recvx uint // receive index 佇列下標,指示下一個被讀取的元素在佇列中的位置
41 recvq waitq // list of recv waiters 等待讀訊息的協程佇列
42 sendq waitq // list of send waiters 等待寫訊息的協程佇列
50 lock mutex // 互斥鎖, chan 不允許併發讀寫
51 }
四個重點:
- 環形佇列
- 等待佇列(讀寫各一個)
- 型別訊息
- 互斥鎖
一般情況下 recvq 和 sendq 至少有一個為空。只有一個例外,那就是同一個協程使用 select 語句向管道一邊寫入資料一邊讀取資料,吃屎協程會分別位於兩個等待佇列中。
向管道寫資料
簡單過程如下:
- 如果緩衝區中有空餘位置,則將資料寫入緩衝區,結束髮送過程。
- 如果緩衝區中沒有空餘位置,則將當前協程加入 sendq 佇列,並進入睡眠並等待被讀協程喚醒
當接收佇列 recvq 不為空時,說明緩衝區中沒有資料但有協程在等待資料,此時會把資料直接傳遞給 recvq 佇列的第一個協程,而不必再寫入緩衝區。
從管道讀資料
簡單過程如下:
- 如果緩衝區中有資料,則從緩衝區中取出資料,結束讀取過程。
- 如果緩衝區沒有資料,則將當前協程加入 recvq 佇列,進入睡眠並等待被寫協程喚醒。
類似的,如果等待傳送佇列 sendq 不為空,且沒有緩衝區,那麼直接從 sendq 佇列的第一個協程中獲取資料
關閉管道
關閉管道會把 recvq 中的協程全部喚醒,這些協程或缺的資料都為 nil。 同時把 sendq 佇列中的協程全部喚醒,但這些協程會觸發 panic
除此之外,其他會觸發 panic 的操作:
- 關閉值為 nil 的管道
- 關閉已經被關閉的管道
- 向已經關閉的管道寫入資料
本作品採用《CC 協議》,轉載必須註明作者和本文連結