Go channel 實現原理分析

guyan0319發表於2019-05-14

channel一個型別管道,通過它可以在goroutine之間傳送和接收訊息。它是Golang在語言層面提供的goroutine間的通訊方式。

眾所周知,Go依賴於稱為CSP(Communicating Sequential Processes)的併發模型,通過Channel實現這種同步模式。Go併發的核心哲學是不要通過共享記憶體進行通訊; 相反,通過溝通分享記憶。

下面以簡單的示例來演示Go如何通過channel來實現通訊。

package main
import (
    "fmt"
    "time"
)
func goRoutineA(a <-chan int) {
    val := <-a
    fmt.Println("goRoutineA received the data", val)
}
func goRoutineB(b chan int) {
    val := <-b
    fmt.Println("goRoutineB  received the data", val)
}
func main() {
    ch := make(chan int, 3)
    go goRoutineA(ch)
    go goRoutineB(ch)
    ch <- 3
    time.Sleep(time.Second * 1)
}

結果為:

goRoutineA received the data 3

上面只是個簡單的例子,只輸出goRoutineA ,沒有執行goRoutineB,說明channel僅允許被一個goroutine讀寫。

接下來我們通過原始碼分析程式執行過程,在講之前,如果不瞭解go 併發和排程相關知識。請閱讀這篇文章

https://github.com/guyan0319/...

說道channel這裡不得不提通道的結構hchan。

hchan

原始碼在src/runtime/chan.go

type hchan struct {
   qcount   uint           // total data in the queue
   dataqsiz uint           // size of the circular queue
   buf      unsafe.Pointer // points to an array of dataqsiz elements
   elemsize uint16
   closed   uint32
   elemtype *_type // element type
   sendx    uint   // send index
   recvx    uint   // receive index
   recvq    waitq  // list of recv waiters
   sendq    waitq  // list of send waiters

   // lock protects all fields in hchan, as well as several
   // fields in sudogs blocked on this channel.
   //
   // Do not change another G's status while holding this lock
   // (in particular, do not ready a G), as this can deadlock
   // with stack shrinking.
   lock mutex
}
type waitq struct {
    first *sudog
    last  *sudog
}

說明:

qcount uint // 當前佇列中剩餘元素個數
dataqsiz uint // 環形佇列長度,即緩衝區的大小,即make(chan T,N),N.
buf unsafe.Pointer // 環形佇列指標
elemsize uint16 // 每個元素的大小
closed uint32 // 表示當前通道是否處於關閉狀態。建立通道後,該欄位設定為0,即通道開啟; 通過呼叫close將其設定為1,通道關閉。
elemtype *_type // 元素型別,用於資料傳遞過程中的賦值;
sendx uint和recvx uint是環形緩衝區的狀態欄位,它指示緩衝區的當前索引 - 支援陣列,它可以從中傳送資料和接收資料。
recvq waitq // 等待讀訊息的goroutine佇列
sendq waitq // 等待寫訊息的goroutine佇列
lock mutex // 互斥鎖,為每個讀寫操作鎖定通道,因為傳送和接收必須是互斥操作。

這裡sudog代表goroutine。

建立channel 有兩種,一種是帶緩衝的channel,一種是不帶緩衝的channel

// 帶緩衝
ch := make(chan Task, 3)
// 不帶緩衝
ch := make(chan int)

這裡我們先討論帶緩衝

ch := make(chan int, 3)

建立通道後的緩衝通道結構

hchan struct {
    qcount uint : 0 
    dataqsiz uint : 3 
    buf unsafe.Pointer : 0xc00007e0e0 
    elemsize uint16 : 8 
    closed uint32 : 0 
    elemtype *runtime._type : &{
        size:8 
        ptrdata:0 
        hash:4149441018 
        tflag:7 
        align:8 
        fieldalign:8 
        kind:130 
        alg:0x55cdf0 
        gcdata:0x4d61b4 
        str:1055 
        ptrToThis:45152
        }
    sendx uint : 0 
    recvx uint : 0 
    recvq runtime.waitq : 
        {first:<nil> last:<nil>}
    sendq runtime.waitq : 
        {first:<nil> last:<nil>}
    lock runtime.mutex : 
        {key:0}
}

原始碼

func makechan(t *chantype, size int) *hchan {

   elem := t.elem
   ...
}

如果我們建立一個帶buffer的channel,底層的資料模型如下圖:

向channel寫入資料

ch <- 3

底層hchan資料流程如圖


傳送操作概要

1、鎖定整個通道結構。

2、確定寫入。嘗試recvq從等待佇列中等待goroutine,然後將元素直接寫入goroutine。

3、如果recvq為Empty,則確定緩衝區是否可用。如果可用,從當前goroutine複製資料到緩衝區。

4、如果緩衝區已滿,則要寫入的元素將儲存在當前正在執行的goroutine的結構中,並且當前goroutine將在sendq中排隊並從執行時掛起。

5、寫入完成釋放鎖。

這裡我們要注意幾個屬性buf、sendx、lock的變化。

流程圖

從channel讀取操作

幾乎和寫入操作相同

程式碼

func goRoutineA(a <-chan int) {
   val := <-a
   fmt.Println("goRoutineA received the data", val)
}

底層hchan資料流程如圖

這裡我們要注意幾個屬性buf、sendx、recvx、lock的變化。

讀取操作概要

1、先獲取channel全域性鎖

2、嘗試sendq從等待佇列中獲取等待的goroutine,

3、 如有等待的goroutine,沒有緩衝區,取出goroutine並讀取資料,然後喚醒這個goroutine,結束讀取釋放鎖。

4、如有等待的goroutine,且有緩衝區(此時緩衝區已滿),從緩衝區隊首取出資料,再從sendq取出一個goroutine,將goroutine中的資料存入buf隊尾,結束讀取釋放鎖。

5、如沒有等待的goroutine,且緩衝區有資料,直接讀取緩衝區資料,結束讀取釋放鎖。

6、如沒有等待的goroutine,且沒有緩衝區或緩衝區為空,將當前的goroutine加入sendq排隊,進入睡眠,等待被寫goroutine喚醒。結束讀取釋放鎖。

流程圖

recvq和sendq 結構

recvq和sendq基本上是連結串列,看起來基本如下

select

select就是用來監聽和channel有關的IO操作,當 IO 操作發生時,觸發相應的動作。

一個簡單的示例如下

package main

import (
   "fmt"
   "time"
)

func goRoutineD(ch chan int, i int) {
   time.Sleep(time.Second * 3)
   ch <- i
}
func goRoutineE(chs chan string, i string) {
   time.Sleep(time.Second * 3)
   chs <- i

}

func main() {
   ch := make(chan int, 5)
   chs := make(chan string, 5)

   go goRoutineD(ch, 5)
   go goRoutineE(chs, "ok")

    select {
    case msg := <-ch:
        fmt.Println(" received the data ", msg)
    case msgs := <-chs:
        fmt.Println(" received the data ", msgs)
    default:
        fmt.Println("no data received ")
        time.Sleep(time.Second * 1)
    }


}

執行程式,因為當前時間沒有到3s,所以select 選擇defult

no data received

修改程式,我們註釋掉default,並多執行幾次結果為

received the data 5

received the data ok

received the data ok

received the data ok

select語句會阻塞,直到監測到一個可以執行的IO操作為止,而這裡goRoutineD和goRoutineE睡眠時間是相同的,都是3s,從輸出可看出,從channel中讀出資料的順序是隨機的。

再修改程式碼,goRoutineD睡眠時間改成4s

func goRoutineD(ch chan int, i int) {
   time.Sleep(time.Second * 4)
   ch <- i
}

此時會先執行goRoutineE,select 選擇case msgs := <-chs。

range

可以持續從channel讀取資料,一直到channel被關閉,當channel中沒有資料時會阻塞當前goroutine,與讀channel時阻塞處理機制一樣。

package main

import (
   "fmt"
   "time"
)

func goRoutineD(ch chan int, i int) {
   for   i := 1; i <= 5; i++{
      time.Sleep(time.Second * 1)
      ch <- i
   }

}
func chanRange(chanName chan int) {
   for e := range chanName {
      fmt.Printf("Get element from chan: %d\n", e)
      if len(chanName) <= 0 { // 如果現有資料量為0,跳出迴圈
            break
      }
   }
}
func main() {
   ch := make(chan int, 5)
   go goRoutineD(ch, 5)
   chanRange(ch)

}

結果:
Get element from chan: 1
Get element from chan: 2
Get element from chan: 3
Get element from chan: 4
Get element from chan: 5

死鎖(deadlock)

指兩個或兩個以上的協程的執行過程中,由於競爭資源或由於彼此通訊而造成的一種阻塞的現象。

在非緩衝通道若發生只流入不流出,或只流出不流入,就會發生死鎖。

下面是一些死鎖的例子

1、

package main

func main() {
   ch := make(chan int)
   ch <- 3
}

上面情況,向非緩衝通道寫資料會發生阻塞,導致死鎖。解決辦法建立緩衝區 ch := make(chan int,3)

2、

package main

import (
   "fmt"
)

func main() {
   ch := make(chan int)
   fmt.Println(<-ch)
}

向非緩衝通道讀取資料會發生阻塞,導致死鎖。 解決辦法開啟緩衝區,先向channel寫入資料。

3、

package main

func main() {
   ch := make(chan int, 3)
   ch <- 3
   ch <- 4
   ch <- 5
   ch <- 6
}

寫入資料超過緩衝區數量也會發生死鎖。解決辦法將寫入資料取走。

4、

package main

func main() {
    ch := make(chan int, 3)
    ch <- 1
    close(ch)
    ch <- 2
}

向關閉的channel寫入資料。解決辦法別向關閉的channel寫入資料。

死鎖的情況有很多這裡不再贅述。

參考:

https://codeburst.io/diving-d...

https://speakerdeck.com/kavya...

https://my.oschina.net/renhc/...

links

相關文章