上篇文章講了 channel
的基本使用,講了一些使用時須要注意的事項,本文將重點介紹 channel
中的兩個數據結構:循環隊列 與 雙端鏈表 。html
爲了理解這些數據結構解決了什麼問題,咱們先來作個簡單的回顧,看看爲何須要這兩個數據結構,他們解決了什麼問題。咱們知道 goroutine 是用戶態的線程,不一樣的 goroutine 之間是有消息傳遞這個需求的。在原始的進程與線程(系統線程)編程中咱們會採用管道的方式,而 channel
就是用戶態線程傳遞消息的管道實現,而且是類型安全的。git
既然 channel
是一個管道,用來知足不一樣 goroutine 間交換消息的。那麼實現這樣一個管道要怎麼作呢?github
來看看咱們平常傳遞消息的需求:golang
channel
進行讀寫,而且保證沒有競爭問題,須要用
隊列 來管理阻塞的
goroutine,解決競爭問題;
channel
的消息(有緩衝、無緩衝),對於有緩存的
channel
能夠採用
循環隊列 來管理多個消息。
固然上面的需求是通過簡化的,好比 channel
還須要具有阻塞、喚醒 goroutine 的能力,不過爲了本文咱們更加專一焦點問題,先只關注上面兩個問題。web
接下來咱們分析一下 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
的結構體被填充成什麼樣子了!安全
注意其中 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
的結構填充狀況。
在這裏能夠看到緩衝的大小是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
的源碼,想要嘗試錄製一個視頻來說這部分源碼!
參考資料
我的公衆號:dayuTalk
GitHub:github.com/helei112g