golang源碼分析--channel

channel的概念

channel是goroutine之間的通信機制,它可以讓一個goroutine通過它給另一個goroutine發送數據,每個channel在創建的時候必須指定一個類型,指定的類型是任意的。
channel 可以看成一個 FIFO 隊列,對 FIFO 隊列的讀寫都是原子的操作,不需要加鎖。
對channel的操作總結:

操作 nil channel closed channel not-closed non-nil channel
close panic panic 成功 close
寫 ch <- 一直阻塞 panic 阻塞或成功寫入數據
讀 <- ch 一直阻塞 讀取對應類型零值 阻塞或成功讀取數據

channel的內部實現

如圖所示,在 channel 的內部實現中(具體定義在 $GOROOT/src/runtime/chan.go 裏),維護了 3 個隊列:

1.讀等待協程隊列 recvq,維護了阻塞在讀此 channel 的協程列表
2.寫等待協程隊列 sendq,維護了阻塞在寫此 channel 的協程列表
3.緩衝數據隊列 buf,用環形隊列實現,不帶緩衝的 channel 此隊列 size 則爲 0

在這裏插入圖片描述
當協程嘗試從未關閉的 channel 中讀取數據時,內部的操作如下:
1.當 buf 非空時,此時 recvq 必爲空,buf 彈出一個元素給讀協程,讀協程獲得數據後繼續執行,此時若 sendq 非空,則從 sendq 中彈出一個寫協程轉入 running 狀態,待寫數據入隊列 buf ,此時讀取操作 <- ch 未阻塞;
2.當 buf 爲空但 sendq 非空時(不帶緩衝的 channel),則從 sendq 中彈出一個寫協程轉入 running 狀態,待寫數據直接傳遞給讀協程,讀協程繼續執行,此時讀取操作 <- ch 未阻塞;
3.當 buf 爲空並且 sendq 也爲空時,讀協程入隊列 recvq 並轉入 blocking 狀態,當後續有其他協程往 channel 寫數據時,讀協程纔會重新轉入 running 狀態,此時讀取操作 <- ch 阻塞。

類似的,當協程嘗試往未關閉的 channel 中寫入數據時,內部的操作如下:
1.當隊列 recvq 非空時,此時隊列 buf 必爲空,從 recvq 彈出一個讀協程接收待寫數據,此讀協程此時結束阻塞並轉入 running 狀態,寫協程繼續執行,此時寫入操作 ch <- 未阻塞;
2.當隊列 recvq 爲空但 buf 未滿時,此時 sendq 必爲空,寫協程的待寫數據入 buf 然後繼續執行,此時寫入操作 ch <- 未阻塞;
3.當隊列 recvq 爲空並且 buf 爲滿時,此時寫協程入隊列 sendq 並轉入 blokcing 狀態,當後續有其他協程從 channel 中讀數據時,寫協程纔會重新轉入 running 狀態,此時寫入操作 ch <- 阻塞。

當關閉 non-nil channel 時,內部的操作如下:
1.當隊列 recvq 非空時,此時 buf 必爲空,recvq 中的所有協程都將收到對應類型的零值然後結束阻塞狀態;
2.當隊列 sendq 非空時,此時 buf 必爲滿,sendq 中的所有協程都會產生 panic ,在 buf 中數據仍然會保留直到被其他協程讀取。

channel的創建

  ch := make(chan int)
  arrch := make(chan int,1)   

channel的使用

1.簡單讀取和寫入
讀取一個已關閉的 channel 時,總是能讀取到對應類型的零值,爲了和讀取非空未關閉 channel 的行爲區別,可以使用兩個接收值:

 ch <- 3
// ok is false when ch is closed
v, ok := <-ch
//普通情況
v := <-ch

2.一對一通知

package main

import (
    "fmt"
    "time"
)

func main() {
    ch := make(chan struct{})
    nums := make([]int, 100)

    go func() {
        time.Sleep(time.Second)
        for i := 0; i < len(nums); i++ {
            nums[i] = i
        }
        // send a finish signal
        ch <- struct{}{}
    }()

    // wait for finish signal
    <-ch
    fmt.Println(nums)
}

3.廣播通知
原理:利用從已關閉的 channel 讀取數據時總是非阻塞的特性,可以實現在一個協程中向其他多個協程廣播某個事件發生的通知

package main

import (
    "fmt"
    "time"
)

func main() {
    N := 10
    exit := make(chan struct{})
    done := make(chan struct{}, N)

    // start N worker goroutines
    for i := 0; i < N; i++ {
        go func(n int) {
            for {
                select {
                // wait for exit signal
                case <-exit:
                    fmt.Printf("worker goroutine #%d exit\n", n)
                    done <- struct{}{}
                    return
                case <-time.After(time.Second):
                    fmt.Printf("worker goroutine #%d is working...\n", n)
                }
            }
        }(i)
    }

    time.Sleep(3 * time.Second)
    // broadcast exit signal
    close(exit)
    // wait for all worker goroutines exit
    for i := 0; i < N; i++ {
        <-done
    }
    fmt.Println("main goroutine exit")
}

channel結構體

可與上文圖片對照觀看

/**
定義了 channel 的結構體
*/
type hchan struct {
	qcount   uint           // 隊列(緩衝區)中的當前數據的個數
	dataqsiz uint           // 循環隊列(緩衝區)中數據大小
	buf      unsafe.Pointer // 數據緩衝區,存放數據的環形數組
	elemsize uint16         // channel 中數據類型的大小 (單個元素的大小)
	closed   uint32         // 表示 channel 是否關閉的標識位
	elemtype *_type // element type  隊列中的元素類型
    // send 和 recieve 的索引,用於實現環形數組隊列
	sendx    uint   // send index    當前發送元素的索引
	recvx    uint   // receive index    當前接收元素的索引
	recvq    waitq  // list of recv waiters    接收等待隊列;由 recv 行爲(也就是 <-ch)阻塞在 channel 上的 goroutine 隊列
	sendq    waitq  // list of send waiters    發送等待隊列;由 send 行爲 (也就是 ch<-) 阻塞在 channel 上的 goroutine 隊列
 
	// lock protects all fields in hchan, as well as several
	// fields in sudogs blocked on this channel.
    // lock保護hchan中的所有字段,以及此通道上阻塞的sudoG中的幾個字段。  
	//
	// 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.
    // 保持此鎖定時不要更改另一個G的狀態(特別是,沒有準備好G),因爲這可能會因堆棧收縮而死鎖。
	lock mutex
}
/**
發送及接收隊列的結構體
等待隊列的鏈表實現
*/
type waitq struct {
	first *sudog
	last  *sudog
}

概括:
channel其實就是由一個環形數組實現的隊列,用於存儲消息元素;兩個鏈表實現的 goroutine 等待隊列,用於存儲阻塞在 recv 和 send 操作上的 goroutine;一個互斥鎖,用於各個屬性變動的同步,只不過這個鎖是一個輕量級鎖。其中 recvq 是讀操作阻塞在 channel 的 goroutine 列表,sendq 是寫操作阻塞在 channel 的 goroutine 列表。列表的實現是 sudog,其實就是一個對 g 的結構的封裝。

參考文章:
https://www.cnblogs.com/tobycnblogs/p/9935465.html
https://blog.csdn.net/qq_25870633/article/details/83388952

發佈了210 篇原創文章 · 獲贊 33 · 訪問量 15萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章