Golang學習筆記-Channel

Golang channel

作為Go的核心的數(shù)據(jù)結(jié)構(gòu)和Goroutine之間的通信,是支撐Go語言高并發(fā)的關(guān)鍵

設(shè)計原理

Go 語言提供了一種不同的并發(fā)模型,也就是通信順序進程(Communicating sequential processes,CSP)1。Goroutine 和 Channel 分別對應 CSP 中的實體和傳遞信息的媒介,Go 語言中的 Goroutine 會通過 Channel 傳遞數(shù)據(jù)。

先入先出

目前Channel收發(fā)操作先入先出的設(shè)計

  1. 先從Channel讀取數(shù)據(jù)的Goroutine會先收到數(shù)據(jù)
  2. 先向 Channel 發(fā)送數(shù)據(jù)的 Goroutine 會得到先發(fā)送數(shù)據(jù)的權(quán)利

無鎖管道

  1. 無鎖(lock-free)隊列更準確的描述是使用樂觀并發(fā)控制的隊列。樂觀并發(fā)控制也叫樂觀鎖,但是它并不是真正的鎖,很多人都會誤以為樂觀鎖是一種真正的鎖,然而它只是一種并發(fā)控制的思想.
    Channel在運行時候,包換了一個用于保護成員變量的互斥鎖,Channel本質(zhì)上是一個用于同步和通信的有鎖隊列,使用互斥鎖解決程序中可能存在的線程競爭問題。
  2. 鎖會導致休眠和喚醒帶來的上下文切換
    • 同步channel 不需要緩沖區(qū),發(fā)送數(shù)據(jù)直接到接收方
    • 異步channel 基于環(huán)形存儲的傳統(tǒng)生產(chǎn)者和消費者模型
    • chan struct{} 類型的異步 Channel — struct{} 類型不占用內(nèi)存空間,不需要實現(xiàn)緩沖區(qū)和直接發(fā)送(Handoff)的語義

數(shù)據(jù)結(jié)構(gòu)

Go 語言的 Channel 在運行時使用 runtime.hchan 結(jié)構(gòu)體表示。我們在 Go 語言中創(chuàng)建新的 Channel 時,實際上創(chuàng)建的都是如下所示的結(jié)構(gòu)體

type hchan struct {
    qcount   uint
    dataqsiz uint
    buf      unsafe.Pointer
    elemsize uint16
    closed   uint32
    elemtype *_type
    sendx    uint
    recvx    uint
    recvq    waitq
    sendq    waitq
    lock     mutex
}
  • qcount Channel中元素的個數(shù)
  • dataqsiz Channel中循環(huán)隊列的長度
  • buf Channel的緩沖區(qū)的數(shù)據(jù)指針
  • sendx Channel 的發(fā)送操作處理到的位置
  • recvx Channel 的接收操作處理到的位置

創(chuàng)建管道

Go 語言中所有 Channel 的創(chuàng)建都會使用 make 關(guān)鍵字。編譯器會將 make(chan int, 10) 表達式被轉(zhuǎn)換成 OMAKE 類型的節(jié)點。

  • 如果當前channel不存在緩沖區(qū),那么就只會為 runtime.hchan 分配一段內(nèi)存空間。
  • 如果當前 Channel 中存儲的類型不是指針類型,就會為當前的 Channel 和底層的數(shù)組分配一塊連續(xù)的內(nèi)存空間
  • 在默認情況下會單獨為 runtime.hchan 和緩沖區(qū)分配內(nèi)存;

發(fā)送數(shù)據(jù)

當我們想要向 Channel 發(fā)送數(shù)據(jù)時,就需要使用 ch <- i 語句,編譯器會將它解析成 OSEND 節(jié)點并在 cmd/compile/internal/gc.walkexpr 函數(shù)中轉(zhuǎn)換成 runtime.chansend1。
在發(fā)送數(shù)據(jù)的邏輯執(zhí)行之前會先為當前 Channel 加鎖,防止發(fā)生競爭條件。如果 Channel 已經(jīng)關(guān)閉,那么向該 Channel 發(fā)送數(shù)據(jù)時就會報"send on closed channel" 錯誤并中止程序。
流程分為下面三部

  1. 當存在等待的接收者時候,通過runtime.send直接將數(shù)據(jù)發(fā)送給阻塞的接收者
  2. 當緩沖區(qū)存在空余空間的時候,將發(fā)送數(shù)據(jù)寫入Channel的緩沖區(qū)
  3. 當不存在緩沖區(qū)或是緩沖區(qū)已滿的時候,等待其他Goroutine從Channel接收數(shù)據(jù)

如果目標 Channel 沒有被關(guān)閉并且已經(jīng)有處于讀等待的 Goroutine,那么 runtime.chansend 函數(shù)會從接收隊列 recvq 中取出最先陷入等待的 Goroutine 并直接向它發(fā)送數(shù)據(jù)。

阻塞發(fā)送

當Channel中沒有接收者能夠處理數(shù)據(jù)的時候,向Channel發(fā)送數(shù)據(jù)就會被下游阻塞,當前使用select關(guān)鍵字可以向Channel非阻塞的發(fā)送消息,向Channel阻塞的發(fā)送數(shù)據(jù)會執(zhí)行下面代碼

func chansend(c *hchan,ep unsafe.Pointer,block bool,callerpc uintptr) bool {
    if !block {
        unlock(&c.lock)
        return false
    }
    gp := getg()
    mysg := acquireSudog()
    mysg.elem = ep
    mysg.g = gp
    mysg.c = c
    gp.waiting = mysq
    c.sendq.enqueue(mysg)
    goparkunlock(&c.lock, waitReasonChanSend, traceEvGoBlockSend, 3)
    gp.waiting = nil
    gp.param = nil
    mysg.c = nil
    releaseSudog(mysg)
    return true
}
  1. 調(diào)用runtime.getg 獲取發(fā)送數(shù)據(jù)使用的 Goroutine;
  2. 執(zhí)行 runtime.acquireSudog 函數(shù)獲取 runtime.sudog 結(jié)構(gòu)體并設(shè)置這一次阻塞發(fā)送的相關(guān)信息,例如發(fā)送的 Channel、是否在 Select 控制結(jié)構(gòu)中和待發(fā)送數(shù)據(jù)的內(nèi)存地址等;
  3. 將剛剛創(chuàng)建并初始化的 runtime.sudog 加入發(fā)送等待隊列,并設(shè)置到當前 Goroutine 的 waiting 上,表示 Goroutine 正在等待該 sudog 準備就緒
  4. 調(diào)用 runtime.goparkunlock 函數(shù)將當前的 Goroutine 陷入沉睡等待喚醒
  5. 被調(diào)度器喚醒后會執(zhí)行一些收尾工作,將一些屬性置零并且釋放 runtime.sudog 結(jié)構(gòu)體

接收數(shù)據(jù)

通過下面兩個方式來接收數(shù)據(jù)

<- ch
ok <- ch
  • 當存在等待的發(fā)送者的時候,通過runtime.recv直接從阻塞的發(fā)送者或者緩沖區(qū)中獲得數(shù)據(jù)
  • 當緩沖區(qū)存在數(shù)據(jù)的時候,從channel的緩沖區(qū)中接收數(shù)據(jù)
  • 當緩沖區(qū)中不存在數(shù)據(jù),等待
  1. 直接接收
    • 當 Channel 的 sendq 隊列中包含處于等待狀態(tài)的 Goroutine 時,該函數(shù)會取出隊列頭等待的 Goroutine,處理的邏輯和發(fā)送時相差無幾,只是發(fā)送數(shù)據(jù)時調(diào)用的是 runtime.send 函數(shù),而接收數(shù)據(jù)時使用 runtime.recv 函數(shù)
      1. 如果不存在緩沖區(qū),則會將數(shù)據(jù)拷貝到目標的內(nèi)存中
      2. 如果存在緩沖區(qū),會將隊列中的數(shù)據(jù)拷貝到接收方的內(nèi)存地址中
  2. 緩沖區(qū)
    • 當channel的緩沖區(qū)中已經(jīng)包含數(shù)據(jù)的時候,從channel中接收數(shù)據(jù)會直接從緩沖區(qū)中的索引位置讀取數(shù)據(jù)并處理。
  3. 阻塞接收
    • 當 Channel 的發(fā)送隊列中不存在等待的 Goroutine 并且緩沖區(qū)中也不存在任何數(shù)據(jù)時,從管道中接收數(shù)據(jù)的操作會變成阻塞操作,然而不是所有的接收操作都是阻塞的,與 select 語句結(jié)合使用時就可能會使用到非阻塞的接收操作。

關(guān)閉管道

編譯器會將用于關(guān)閉管道的 close 關(guān)鍵字轉(zhuǎn)換成 OCLOSE 節(jié)點以及 runtime.closechan 的函數(shù)調(diào)用。當 Channel 是一個空指針或者已經(jīng)被關(guān)閉時,Go 語言運行時都會直接 panic 并拋出異

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容