上篇文章講了 channel 的基本使用,講了一些使用時需要注意的事項,本文將重點介紹 channel 中的兩個數據結構:循環隊列 與 雙端鍊表 。
為了理解這些數據結構解決了什麼問題,我們先來做個簡單的回顧,看看為什麼需要這兩個數據結構,他們解決了什麼問題。我們知道 goroutine 是用戶態的線程,不同的 goroutine 之間是有消息傳遞這個需求的。在原始的進程與線程(系統線程)編程中我們會採用管道的方式,而 channel 就是用戶態線程傳遞消息的管道實現,並且是類型安全的。
既然 channel 是一個管道,用來滿足不同 goroutine 間交換消息的。那麼實現這樣一個管道要怎麼做呢?
來看看我們日常傳遞消息的需求:
能夠有多個 goroutine 向 channel 進行讀寫,並且保證沒有競爭問題,需要用 隊列 來管理阻塞的 goroutine,解決競爭問題;需要管理髮送到 channel 的消息(有緩衝、無緩衝),對於有緩存的 channel 可以採用 循環隊列 來管理多個消息。當然上面的需求是經過簡化的,比如 channel 還需要具備阻塞、喚醒 goroutine 的能力,不過為了本文我們更加專注焦點問題,先只關註上面兩個問題。
channel 的數據結構接下來我們分析一下 channel 在實際運行中,它的結構體是怎麼樣的。當然這又分為兩種類型,有緩衝與無緩衝的。我們先來看一個無緩衝的情況。
無緩衝先把示例代碼貼出來。就是兩個讀的 goroutine 被阻塞在一個無緩衝的 channel 上。
func main() {
ch := make(chan int) // 無緩衝
go goRoutineA(ch)
go goRoutineB(ch)
ch <- 1
time.Sleep(time.Second * 1)
}
func goRoutineA(ch chan int) {
v := <-ch
fmt.Printf("A received data: %d\n", v)
}
func goRoutineB(ch chan int) {
v := <-ch
fmt.Printf("B received data: %d\n", v)
}來看看當代碼執行到 ch <- 1 這一行之後 channel 的結構體被填充成什麼樣子了!
無緩衝 channel注意其中 buf 欄位可存儲的長度是0,這是因為 無緩衝 channel 不會用到循環隊列來存儲數據。它一定是等讀、寫 goroutine 都準備好了,然後直接把數據交給對方。我們用一副圖來看一下無緩衝的數據交換過程。
交換數據上圖描述的是數據交換過程,再看一下讀 goroutine 被阻塞的結構示意圖。被阻塞的 goroutine 會掛載到對應的隊列上,該隊列是一個雙端隊列。
上面的例子,由於兩個讀 goroutine 在啟動的時候,寫還沒有準備好,因此讀全部被掛起在隊列中;當有寫goroutine準備好的時候,由於此時讀已經就緒,因此寫不會阻塞,掛起放到 sendq 中。大家可以修改上面的代碼,自己看一下寫阻塞,讀立馬執行的情況。
結構示例有緩衝我們將上面的代碼改成有緩衝的通道,然後再來看看有緩衝的情況。
func main() {
ch := make(chan int, 3) // 有緩衝
// 都不會阻塞
ch <- 1
ch <- 2
ch <- 3
// 會阻塞,被掛起到 sendq 中
go func() {
ch <- 4
}()
// 只是為了debug
var a int
fmt.Println(a)
go goRoutineA(ch)
go goRoutineA(ch)
go goRoutineB(ch)
go goRoutineB(ch) // 猜猜這裡會被掛起嗎?
time.Sleep(time.Second * 2)
}
func goRoutineA(ch chan int) {
v := <-ch
fmt.Printf("A received data: %d\n", v)
}
func goRoutineB(ch chan int) {
v := <-ch
fmt.Printf("B received data: %d\n", v)
}貼出執行到第一行的 go goRoutineA(ch) 時, hchan 的結構填充情況。
有緩衝 channel在這裡可以看到緩衝的大小是3,由於增加了緩衝,只要寫 goroutine 沒有把緩衝寫滿,則不會導致協程阻塞。但是一旦緩衝沒有多餘的空間,則會把寫 goroutine 掛起到 sendq 中,直到有空間時將他喚醒(還有其它喚醒的場景,這一略過)。
其實有緩衝的 channel,就是把同步的通信變為了異步的通信。寫的 channel 不需要關注讀 channel,只要有空間它就寫;而讀也一樣,只要有數據就正常讀就可以,如果沒有就掛起到隊列中,等待被喚醒。下圖形象的展示了有緩衝 channel 是如何交換數據的。
交換數據我們再來用圖的形式看一下此時結構體的樣子,這裡圖有些偷懶,只是在上面圖的基礎上增加了循環隊列部分的描述,實際到該例子中,讀 goroutine時不會被阻塞的,看的時候需要注意這一點。
結構示例循環隊列今天最重要的是理解 channel 中兩個關鍵的數據結構。為了下一講閱讀源碼做準備,我把 channel 中的循環隊列部分的代碼抽象出來了。
// 隊列滿了
var ErrQFull = errors.New("circular is full")
// 沒有值
var ErrQEmpty = errors.New("circular is empty")
// 定義循環隊列
// 如何確定隊空,還是隊滿?q.sendx = (q.sendx+1) % q.dataqsiz
type queue struct {
buf []int // 隊列元素存儲
dataqsiz uint // circular 隊列長度
qcount uint // 有多少元素在buf中 qcount = len(buf)
sendx uint // 可以理解為隊尾指針,向隊列寫入數據
recvx uint // 可以理解為隊頭指針,從隊列讀取數據
}
func makeQ(size int) *queue {
q := &queue{
dataqsiz: uint(size),
buf: nil,
}
q.buf = make([]int, q.dataqsiz)
return q
}
// 向buf中寫入數據
// 請看 chansend 函數
func (c *queue) insert(ele int) error {
// 檢查隊列是否有空間
if c.dataqsiz > 0 && c.qcount == c.dataqsiz {
return ErrQFull
}
// 存入數據
c.buf[c.sendx] = ele
c.sendx++ // 尾指針後移
if c.sendx == c.dataqsiz { // 如果相等,說明隊列寫滿了,sendx放到開始位置
c.sendx = 0
}
c.qcount++
return nil
}
// 從buf中讀取數據
func (c *queue) read() (int, error) {
// 隊列中沒有數據了
if c.dataqsiz > 0 && c.qcount == 0 {
return 0, ErrQEmpty
}
ret := c.buf[c.recvx] // 取出元素
c.buf[c.recvx] = 0
c.recvx++
if c.recvx == c.dataqsiz { // 如果相等,說明寫到末尾了,recvx放到開始位置
c.recvx = 0
}
c.qcount--
return ret, nil
}上面的代碼基本上就是 channel 的循環隊列部分的實現。這個隊列的實現與我們平常實現的循環隊列稍微有些不一樣。一般我們為了方便判空,會浪費一個buf的空間來方便判空,公式是:(tail+1)%n=head ;但是在 channel 這裡的循環隊列,由於有了一個循環隊列元素的計數,確保了這個空間不會被浪費,並且同時也能夠滿足 O(1) 時間複雜度計算有緩衝 channel 元素個數。
總結總結一下今天的主要信息。
channel 中用到了兩個數據結構:循環隊列 和 雙端鍊表;循環隊列 只有在有緩衝 channel 中才會使用,它主要是做為消息的緩衝、保證消息的有序性;雙端鍊表 是用來掛起阻塞的讀、寫 goroutine 的,在被喚醒時會按照入隊順序公平的進行通知;無緩衝的 channel 不會用到 循環隊列 相關的結構,它必須讀寫 goroutine 都準備好後才能進行消息交換;做為緩衝消息的 循環隊列 通過一個當前元素個數欄位的標記,避免了浪費一個數據空間。下一章我們就嘗試閱讀一下 channel 的源碼,想要嘗試錄製一個視頻來講這部分源碼!
參考資料
[1] [Diving Deep Into The Golang Channels.](https://codeburst.io/diving-deep-into-the-golang-channels-549fd4ed21a8)
[2] [Understanding Channels](https://speakerdeck.com/kavya719/understanding-channels)
[3] [The Nature Of Channels In Go](https://www.ardanlabs.com/blog/2014/02/the-nature-of-channels-in-go.html)