chan資料結構與理解

水墨先生發表於2021-10-02

關於對channel的理解,我們先看看channel的資料結構是怎樣的吧

type hchan struct {
    2. qcount uint // 當前佇列中剩餘元素個數 
    3. dataqsiz uint // 環形佇列長度,即可以存放的元素個數
    4. buf unsafe.Pointer // 環形佇列指標
    5. elemsize uint16// 每個元素的大小
    6. closed uint32 // 標識關閉狀態 
    7. elemtype *_type // 元素型別 
    8. sendx uint // 佇列下標,指示元素寫入時存放到佇列中的位置 
    9. recvx uint // 佇列下標,指示元素從佇列的該位置讀出 
    10. recvq waitq // 等待讀訊息的goroutine佇列
    11. sendq waitq // 等待寫訊息的goroutine佇列 
    12. lock mutex // 互斥鎖,chan不允許併發讀寫 13.
}

從上面的channel資料結構可以看出channel由佇列、型別資訊、goroutine等待佇列組成。
可以看出chan內部實現了一個環形佇列作為其緩衝區,佇列的長度是建立chan時指定的。

chan資料結構與理解
看一下上面這張圖可以這樣理解:

  • dataqsiz指定了佇列長度為6,說明chan可快取6個元素;
  • buf指向佇列的記憶體,佇列中還剩下兩個元素;
  • qcount表示佇列中還有兩個元素;
  • sendx指示後續寫入的資料儲存的位置,取值(0,6);
  • recvx指示從該位置讀取資料, 取值[0, 6);

channel會在以下情況將goroutine堵塞:

  • 從channel讀資料,如果channel緩衝區為空或者沒有緩衝區,當前goroutine會被阻塞。
  • 向channel寫資料,如果channel緩衝區已滿或者沒有緩衝區,當前goroutine會被阻塞。
  • 被阻塞的goroutine將會掛在channel的等待佇列中

被channel堵塞的goroutine如何喚醒:

  • 因讀阻塞的goroutine會被向channel寫入資料的goroutine喚醒;
  • 因寫阻塞的goroutine會被從channel讀資料的goroutine喚醒;
    chan資料結構與理解
  • 上圖展示了一個沒有緩衝區的channel,有幾個goroutine阻塞等待讀資料
  • 需要特別注意的是一般情況下recvq和sendq至少有一個為空。只有一個例外,那就是同一個goroutine使用select語句向 channel一邊寫資料,一邊讀資料

channel的型別限制

  • 一個channel只能傳遞一種型別的值,型別資訊儲存在hchan資料結構中。
  • elemtype代表型別,用於資料傳遞過程中的賦值;
  • elemsize代表型別大小,用於在buf中定位元素位置;

channel鎖機制

  • 這個很好理解,一個channel同時僅允許被一個goroutine讀寫

建立channel的過程實際上是初始化hchan結構。其中型別資訊和緩衝區長度由make語句傳入,buf的大小則與元素大小和緩衝區長度共同決定,以下程式碼演示瞭如何建立一個偽channel:

func makechan(t *chantype, size int) *hchan { 
    2. var c *hchan 
    3. c = new(hchan) 
    4. c.buf = malloc(元素型別大小*size) 
    5. c.elemsize = 元素型別大小 
    6. c.elemtype = 元素型別 
    7. c.dataqsiz = size
    8. return c}

如何向channel寫資料:

  • 等待接收佇列recvq不為空,說明緩衝區中沒有資料或者沒有緩衝區,此時直接從recvq取出G,並把資料寫入,最後把該G喚醒,結束髮送過程;
  • 如果緩衝區中有空餘位置,將資料寫入緩衝區,結束髮送過程;
  • 如果緩衝區中沒有空餘位置,將待傳送資料寫入G,將當前G加入sendq,進入睡眠,等待被讀goroutine喚醒;

chan資料結構與理解

  • 上面的圖很好的演示了channel寫資料的過程,不理解的可以多看幾遍

如何從channel中讀資料

  • 等待傳送佇列sendq不為空,且沒有緩衝區,直接從sendq中取出G,把G中資料讀出,最後把G喚醒,結束讀取過程;
  • 等待傳送佇列sendq不為空,此時說明緩衝區已滿,從緩衝區中首部讀出資料,把G中資料寫入緩衝區尾部,把G喚醒,結束讀取過程;
  • 如果緩衝區中有資料,則從緩衝區取出資料,結束讀取過程;
  • 將當前goroutine加入recvq,進入睡眠,等待被寫goroutine喚醒;

chan資料結構與理解

  • 以上圖很好的說明了channel是如何將資料讀取出來的,不懂多看幾遍哈

關閉channel應該避免以下的場景出現:

  • 以下幾種場景會出現panic:
    1. 關閉值為nil的channel
    2. 關閉已經被關閉的channel
    3. 向已經關閉的channel寫資料

以上這些是我本人通過查詢資料整理出來關於對channel的理解,如有不足之處還望指點糾正。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章