Go語言學習 - Chan的工做原理

咱們建立了一大堆線程, 如今咱們想要實現線程間的同步, 這其中的關鍵就是chan(通道)的使用, 若是沒有通道, 你應該怎麼去作線程間同步呢? time.Sleep嗎?app

Introduction

type hchan struct {
  dataq_size uint            // 緩衝槽大小
  buf        unsafe.Pointer  // 緩衝槽本體
  elem_type  *_type          // 槽內數據類型
}
複製代碼

緩衝槽的工做方式就是上圖那樣, 每當你往通道里寫消息, 消息會先存到緩衝槽裏, 然後才被取出來. 這種常規的工做模式也叫異步模式, 由於收發工做不是同步進行的, 你能夠先發, 發完你走人, 隨後收件人再去管道里取.dom

一樣還有一種用法是不設置緩衝槽, 或者說你直接把緩衝槽大小設置成0, 這種工做模式下, 收發雙方必須同時守在管道旁, 不然先到的人必定會堵塞在管道哪兒, 等待後到的人, 而後通訊才能開始, 這種也叫同步模式異步

仔細想想通道給咱們帶來了什麼, 若是將通道的功能點拆開, 通道的核心功能點就是: 能夠在兩個不一樣的G之間相互發消息, 同時還有一套阻塞/喚醒的機制.函數

通道的工做模式

首先分析一個關鍵點, 你須要知道有哪些G正守着這個通道, 而後把管道里的參數拷貝給這些堵塞着的G, 最後去解除他們的阻 . 有了這個前提條件, 通道工做圍繞的對象就必定是G,ui

type hchan struct {
  recv_q waitq  // 接收者隊列
  send_q waitq  // 發送者隊列
}

type waitq struct {
  first *sudog  
  last  *sudog
}

type sudog struct {
  g     *g               // 想要收發消息的g本體
  elem  unsafe.Pointer   // 要發的消息本體
}
複製代碼

每一個通道都會維護一個發送者隊裏以及一個接收者隊列, 隊列裏的元素是一個包裝過的G(G本體+消息). 咱們經過一個發送與接收的過程, 先說說同步通道是如何工做的, 異步通道於此相似spa

收發 - 同步模式

chan <- data的操做會被編譯器翻譯成去執行chansend函數, 執行的對象是名爲chan的通道, 攜帶一個消息結構體. 檢查chan的緩衝槽長度爲0, 進入通道的同步模式工做:線程

  • 找一個接收者, 檢查chan的接收者隊列
  • chan.recv_q爲空, 按照同步隊列的工做模式, 由於沒人接消息, 這個G必須阻塞(沉睡), 你這個G, 連同你的消息, 一塊兒被打包成一個sudog, 放到chan的發送者隊列裏
    • 而後經過gopark把你放入沉睡模式
  • 但若是有接收者, 取出這個sudog對象, 而後把這條消息拷貝到sudog.elem裏面, 這表明我這條消息已經發給你了.
    • 最後由於這個接收者以前由於沒收到消息一直在沉睡中, 經過goready喚醒

ok以上說明了幾件事, 通道是如何知道消息應該發給誰, 消息是怎麼發送過去的, 以及咱們看到的阻塞效果是怎麼實現的.翻譯

仔細想一想, 這阻塞? 在某種條件達到之後自動解除阻塞? 這個場景好像在哪裏見過? 你小子在暗示sync.WaitGroup!! 等wg.Count變成0了以後自動解除阻塞, wg使用的阻塞效果也正是經過gopark實現的! 這種沉睡/阻塞最大的特色就是, 某個G被放入沉睡之後, 必須由你手動喚醒, 在咱們的場景中這個條件就是找到了接收方, 在wg的場景中這個條件就是wg.Count變成0了, 條件一命中, 我手動馬上幫你喚醒並解除阻塞.3d

收發 - 異步模式

想象一下異步模式與同步模式的區別在哪? 惟一的區別, 僅僅是在管道填滿了纔會產生堵塞, 否則你發完/收完就走人.code

剩下來原理基本同樣, chan <- data操做一樣被翻譯成chansend函數的調用, 發現緩衝槽大小不爲0之後進入異步工做模式, 開始檢查緩衝槽的剩餘艙位

  • 若是還有剩餘艙位, 將消息經過memmove拷貝到管道內, 而後若是發現還有接收者正在堵塞中, 經過goready喚醒
  • 若是沒有剩餘艙位, 經過gopark進入休眠模式, 在被喚醒之後檢查有沒有數據

關閉

到了這兒你已經對這一套工做模式很是瞭解了, 所謂的關閉其實就是遍歷通道的發送者隊列+接收者隊列, 在他們的數據區發送一條nil消息, 而後執行goready喚醒他們中的每個人

Select的工做模式

常常與通道一塊兒出現的就是Select, Select的功能只是: 從全部的通道case中隨機挑一個能用的, 不然就一直堵塞直到出現一個能用的. 咱們解析一下這種特性是怎麼作到的.

隨機序

type hselect struct {
  ncases     uint16  // 總數
  poll_order *uint16 // 隨機序號
  cases      []scase // 按照初始化順序的case隊列
}
type scase struct {
  c    *hchan // case的本體, 一個通道
  kind uint16 // 通道類型
}
複製代碼

一個select在初始化的時候, 而後把全部的通道從chan類型包裝成scase類型, 添加上一個字段叫作Kind,這個字段能夠是"接收者通道"/"發送者通道", 最後還有一個"default"類型通道, 代表這是一個default case.

而後會生成一個隨機序號存到poll_order字段中去, 這表明一個隨機數, 而後等程序運行到select的位置的時候, 調用select_go函數, 開始找能夠用的通道:

for i,_ := range [0...ncases] {
    random_case_id := poll_order[i]
    random_chanel  := cases[random_id]
    if check(random_chanel) {
        return 
    }
}
if default_case != nil {
    execute(default_case)
    return
}
複製代碼

咱們按照以上的方法去執行隨機序, 在全部的case都遍歷完了之後, 若是沒用能用的, 檢查有沒有能用的default用

沉睡的select

咱們已經知道通道的沉睡與喚醒是怎麼實現的, 針對select有意思的一點是, 若是子通道被喚醒, 則本身這個selectG也同時被喚醒了. 這點很神奇, 怎麼作到的

for i,_ := range [0...ncases] {
    random_case_id := poll_order[i]
    random_chanel  := cases[random_id]
    if random_chanel.kind == recv_chanel {
        random_chanel.recvq.append(selG)
    }
    if random_chanel.kind == send_chanel {
        random_chanel.sendq.append(selG)
    }
}

gopark(selectG)
複製代碼

一樣的, 咱們也是遍歷select下的全部通道, 把本身添加到通道的消息隊列中去

  • 若是是它是發送類型通道, 那就在它的發送者隊列中添加本身這個selectG
  • 若是是它是接收類型通道, 那就在它的接收者隊列中添加本身這個selectG

想想這樣作會發生什麼, 本身這個SelectG協程會同時出如今不少通道的消息隊列裏, 其中任何一個通道被goready喚醒的時候, 本身這個SelectG也會被通知到, 本身也會跟這個通道一塊兒被喚醒. 這就實現了select會一直堵塞直到其中任何一個通道暢通爲止的特性

相關文章
相關標籤/搜索