channel的概念
channel是goroutine之間的通信機制,它可以讓一個goroutine通過它給另一個goroutine發送數據,每個channel在創建的時候必須指定一個類型,指定的類型是任意的。
channel 可以看成一個 FIFO 隊列,對 FIFO 隊列的讀寫都是原子的操作,不需要加鎖。
對channel的操作總結:
操作 | nil channel | closed channel | not-closed non-nil channel |
---|---|---|---|
close | panic | panic | 成功 close |
寫 ch <- | 一直阻塞 | panic | 阻塞或成功寫入數據 |
讀 <- ch | 一直阻塞 | 讀取對應類型零值 | 阻塞或成功讀取數據 |
channel的內部實現
如圖所示,在 channel 的內部實現中(具體定義在 $GOROOT/src/runtime/chan.go 裏),維護了 3 個隊列:
1.讀等待協程隊列 recvq,維護了阻塞在讀此 channel 的協程列表
2.寫等待協程隊列 sendq,維護了阻塞在寫此 channel 的協程列表
3.緩衝數據隊列 buf,用環形隊列實現,不帶緩衝的 channel 此隊列 size 則爲 0
當協程嘗試從未關閉的 channel 中讀取數據時,內部的操作如下:
1.當 buf 非空時,此時 recvq 必爲空,buf 彈出一個元素給讀協程,讀協程獲得數據後繼續執行,此時若 sendq 非空,則從 sendq 中彈出一個寫協程轉入 running 狀態,待寫數據入隊列 buf ,此時讀取操作 <- ch 未阻塞;
2.當 buf 爲空但 sendq 非空時(不帶緩衝的 channel),則從 sendq 中彈出一個寫協程轉入 running 狀態,待寫數據直接傳遞給讀協程,讀協程繼續執行,此時讀取操作 <- ch 未阻塞;
3.當 buf 爲空並且 sendq 也爲空時,讀協程入隊列 recvq 並轉入 blocking 狀態,當後續有其他協程往 channel 寫數據時,讀協程纔會重新轉入 running 狀態,此時讀取操作 <- ch 阻塞。
類似的,當協程嘗試往未關閉的 channel 中寫入數據時,內部的操作如下:
1.當隊列 recvq 非空時,此時隊列 buf 必爲空,從 recvq 彈出一個讀協程接收待寫數據,此讀協程此時結束阻塞並轉入 running 狀態,寫協程繼續執行,此時寫入操作 ch <- 未阻塞;
2.當隊列 recvq 爲空但 buf 未滿時,此時 sendq 必爲空,寫協程的待寫數據入 buf 然後繼續執行,此時寫入操作 ch <- 未阻塞;
3.當隊列 recvq 爲空並且 buf 爲滿時,此時寫協程入隊列 sendq 並轉入 blokcing 狀態,當後續有其他協程從 channel 中讀數據時,寫協程纔會重新轉入 running 狀態,此時寫入操作 ch <- 阻塞。
當關閉 non-nil channel 時,內部的操作如下:
1.當隊列 recvq 非空時,此時 buf 必爲空,recvq 中的所有協程都將收到對應類型的零值然後結束阻塞狀態;
2.當隊列 sendq 非空時,此時 buf 必爲滿,sendq 中的所有協程都會產生 panic ,在 buf 中數據仍然會保留直到被其他協程讀取。
channel的創建
ch := make(chan int)
arrch := make(chan int,1)
channel的使用
1.簡單讀取和寫入
讀取一個已關閉的 channel 時,總是能讀取到對應類型的零值,爲了和讀取非空未關閉 channel 的行爲區別,可以使用兩個接收值:
ch <- 3
// ok is false when ch is closed
v, ok := <-ch
//普通情況
v := <-ch
2.一對一通知
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan struct{})
nums := make([]int, 100)
go func() {
time.Sleep(time.Second)
for i := 0; i < len(nums); i++ {
nums[i] = i
}
// send a finish signal
ch <- struct{}{}
}()
// wait for finish signal
<-ch
fmt.Println(nums)
}
3.廣播通知
原理:利用從已關閉的 channel 讀取數據時總是非阻塞的特性,可以實現在一個協程中向其他多個協程廣播某個事件發生的通知
package main
import (
"fmt"
"time"
)
func main() {
N := 10
exit := make(chan struct{})
done := make(chan struct{}, N)
// start N worker goroutines
for i := 0; i < N; i++ {
go func(n int) {
for {
select {
// wait for exit signal
case <-exit:
fmt.Printf("worker goroutine #%d exit\n", n)
done <- struct{}{}
return
case <-time.After(time.Second):
fmt.Printf("worker goroutine #%d is working...\n", n)
}
}
}(i)
}
time.Sleep(3 * time.Second)
// broadcast exit signal
close(exit)
// wait for all worker goroutines exit
for i := 0; i < N; i++ {
<-done
}
fmt.Println("main goroutine exit")
}
channel結構體
可與上文圖片對照觀看
/**
定義了 channel 的結構體
*/
type hchan struct {
qcount uint // 隊列(緩衝區)中的當前數據的個數
dataqsiz uint // 循環隊列(緩衝區)中數據大小
buf unsafe.Pointer // 數據緩衝區,存放數據的環形數組
elemsize uint16 // channel 中數據類型的大小 (單個元素的大小)
closed uint32 // 表示 channel 是否關閉的標識位
elemtype *_type // element type 隊列中的元素類型
// send 和 recieve 的索引,用於實現環形數組隊列
sendx uint // send index 當前發送元素的索引
recvx uint // receive index 當前接收元素的索引
recvq waitq // list of recv waiters 接收等待隊列;由 recv 行爲(也就是 <-ch)阻塞在 channel 上的 goroutine 隊列
sendq waitq // list of send waiters 發送等待隊列;由 send 行爲 (也就是 ch<-) 阻塞在 channel 上的 goroutine 隊列
// lock protects all fields in hchan, as well as several
// fields in sudogs blocked on this channel.
// lock保護hchan中的所有字段,以及此通道上阻塞的sudoG中的幾個字段。
//
// Do not change another G's status while holding this lock
// (in particular, do not ready a G), as this can deadlock
// with stack shrinking.
// 保持此鎖定時不要更改另一個G的狀態(特別是,沒有準備好G),因爲這可能會因堆棧收縮而死鎖。
lock mutex
}
/**
發送及接收隊列的結構體
等待隊列的鏈表實現
*/
type waitq struct {
first *sudog
last *sudog
}
概括:
channel其實就是由一個環形數組實現的隊列,用於存儲消息元素;兩個鏈表實現的 goroutine 等待隊列,用於存儲阻塞在 recv 和 send 操作上的 goroutine;一個互斥鎖,用於各個屬性變動的同步,只不過這個鎖是一個輕量級鎖。其中 recvq 是讀操作阻塞在 channel 的 goroutine 列表,sendq 是寫操作阻塞在 channel 的 goroutine 列表。列表的實現是 sudog,其實就是一個對 g 的結構的封裝。
參考文章:
https://www.cnblogs.com/tobycnblogs/p/9935465.html
https://blog.csdn.net/qq_25870633/article/details/83388952