首頁 >後端開發 >Golang >Go 頻道解鎖:它們是如何運作的

Go 頻道解鎖:它們是如何運作的

Mary-Kate Olsen
Mary-Kate Olsen原創
2025-01-17 02:11:10362瀏覽

深入Golang Channel:實現原理及效能最佳化建議

Golang的Channel是其CSP並發模式的關鍵組成部分,也是Goroutine之間通訊的橋樑。 Channel在Golang中被頻繁使用,深入了解其內部實現原理至關重要。本文將基於Go 1.13源碼分析Channel的底層實作。

Channel基礎用法

在正式分析Channel實作之前,先回顧其基本用法:

<code class="language-go">package main
import "fmt"

func main() {
    c := make(chan int)

    go func() {
        c <- 1 // 发送操作
    }()

    x := <-c // 接收操作
    fmt.Println(x)
}</code>

這段程式碼展示了Channel的兩個基本操作:

  • 傳送操作:c <- 1
  • 接收操作:x := <-c

Channel分為緩衝Channel和非緩衝Channel。上述程式碼使用了非緩衝Channel。在非緩衝Channel中,如果目前沒有其他Goroutine接收數據,則發送方會在傳送語句處阻塞。

初始化Channel時可以指定緩衝區大小,例如make(chan int, 2)指定緩衝區大小為2。在緩衝區未滿之前,發送方可以無阻塞地發送數據,無需等待接收方準備好。但如果緩衝區已滿,發送方仍然會阻塞。

Channel底層實作函數

在深入Channel原始碼之前,需要先找到Golang中Channel的具體實作位置。使用Channel時,實際上呼叫的是runtime.makechanruntime.chansendruntime.chanrecv等底層函數。

可以使用go tool compile -N -l -S hello.go指令將程式碼轉換為組譯指令,或使用線上工具Compiler Explorer (例如:go.godbolt.org/z/3xw5Cj)。透過分析彙編指令,可以發現:

  • make(chan int)對應runtime.makechan函數。
  • c <- 1對應runtime.chansend函數。
  • x := <-c對應runtime.chanrecv函數。

這些函數的實作都位於Go原始碼的runtime/chan.go檔案中。

Channel構造

make(chan int)會被編譯器轉換為runtime.makechan函數,其函數簽章如下:

<code class="language-go">func makechan(t *chantype, size int) *hchan</code>

其中,t *chantype是Channel元素類型,size int是使用者指定的緩衝區大小(未指定則為0),傳回值為*hchanhchan是Golang中Channel的內部實作結構體,定義如下:

<code class="language-go">type hchan struct {
        qcount   uint           // 缓冲区中已放入元素的数量
        dataqsiz uint           // 用户构造Channel时指定的缓冲区大小
        buf      unsafe.Pointer // 缓冲区
        elemsize uint16         // 缓冲区中每个元素的大小
        closed   uint32         // Channel是否关闭,==0表示未关闭
        elemtype *_type         // Channel元素的类型信息
        sendx    uint           // 缓冲区中发送元素的索引位置(发送索引)
        recvx    uint           // 缓冲区中接收元素的索引位置(接收索引)
        recvq    waitq          // 等待接收的Goroutine列表
        sendq    waitq          // 等待发送的Goroutine列表

        lock mutex
}</code>

hchan中的屬性大致分為三類:

  • 緩衝區相關屬性: 如buf, dataqsiz, qcount等。當Channel的緩衝區大小不為0時,緩衝區用於儲存待接收的數據,使用環形緩衝區實作。
  • 等待佇列相關屬性: recvq包含等待接收資料的Goroutine,sendq包含等待傳送資料的Goroutine。 waitq使用雙向鍊錶實作。
  • 其他屬性: 如lock, elemtype, closed等。

makechan函數主要進行一些合法性檢查和緩衝區、hchan等屬性的記憶體分配,這裡不再深入討論。

基於hchan屬性的簡單分析,可以看出其中有兩個重要的組成部分:緩衝區和等待隊列。 hchan的所有行為和實現都圍繞著這兩個組成部分。

Channel資料發送

Channel的傳送和接收過程非常相似。先分析Channel的發送過程(例如c <- 1)。

嘗試傳送資料至Channel時,如果recvq佇列不為空,則會先從recvq頭部取出一個等待接收資料的Goroutine,並將資料直接傳送給該Goroutine。程式碼如下:

<code class="language-go">package main
import "fmt"

func main() {
    c := make(chan int)

    go func() {
        c <- 1 // 发送操作
    }()

    x := <-c // 接收操作
    fmt.Println(x)
}</code>

recvq包含等待接收資料的Goroutine。當一個Goroutine使用接收操作(例如x := <-c)時,如果此時sendq不為空,則會從sendq中取出一個Goroutine,並將資料傳送給它。

如果recvq為空,表示此時沒有Goroutine等待接收數據,Channel會嘗試將數據放入緩衝區:

<code class="language-go">func makechan(t *chantype, size int) *hchan</code>

這段程式碼的功能很簡單,就是將資料放入緩衝區。這個過程涉及環形緩衝區的操作,dataqsiz表示使用者指定的緩衝區大小(未指定則預設為0)。

如果使用的是非緩衝Channel或緩衝區已滿(c.qcount == c.dataqsiz),則會將待發送的資料和目前Goroutine打包成sudog對象,放入sendq,並將目前Goroutine設定為等待狀態:

<code class="language-go">type hchan struct {
        qcount   uint           // 缓冲区中已放入元素的数量
        dataqsiz uint           // 用户构造Channel时指定的缓冲区大小
        buf      unsafe.Pointer // 缓冲区
        elemsize uint16         // 缓冲区中每个元素的大小
        closed   uint32         // Channel是否关闭,==0表示未关闭
        elemtype *_type         // Channel元素的类型信息
        sendx    uint           // 缓冲区中发送元素的索引位置(发送索引)
        recvx    uint           // 缓冲区中接收元素的索引位置(接收索引)
        recvq    waitq          // 等待接收的Goroutine列表
        sendq    waitq          // 等待发送的Goroutine列表

        lock mutex
}</code>

goparkunlock會解鎖輸入的互斥鎖並掛起當前Goroutine,將其設定為等待狀態。 goparkgoready是成對出現的,是互逆的操作。

從使用者角度來看,呼叫gopark後,傳送資料的程式碼語句會阻塞。

Channel資料接收

Channel的接收過程與發送過程基本上類似,這裡不再贅述。接收過程中涉及的緩衝區相關操作會在後面詳細描述。

要注意的是,Channel的整個傳送和接收過程都使用了runtime.mutex進行加鎖。 runtime.mutex是runtime相關原始碼中常用的輕量級鎖,整個過程並非最高效的無鎖方案。 Golang中存在一個關於無鎖Channel的issue:go/issues#8899。

Channel環形緩衝區實作

Channel使用環形緩衝區快取寫入的資料。環形緩衝區具有諸多優點,非常適合實現固定長度的FIFO佇列。

Channel中環形緩衝區的實作如下:

hchan中與緩衝區相關的兩個變數:recvxsendxsendx表示緩衝區中可寫入的索引,recvx表示緩衝區中可讀的索引。 recvxsendx之間的元素表示已正常放入緩衝區的資料。

Go Channel Unlocked: How They Work

可以直接使用buf[recvx]讀取佇列的第一個元素,使用buf[sendx] = x將元素放入佇列的末端。

緩衝區寫入

當緩衝區未滿時,將資料放入緩衝區的操作如下:

<code class="language-go">package main
import "fmt"

func main() {
    c := make(chan int)

    go func() {
        c <- 1 // 发送操作
    }()

    x := <-c // 接收操作
    fmt.Println(x)
}</code>

chanbuf(c, c.sendx)等價於c.buf[c.sendx]。上述過程很簡單,就是將資料複製到緩衝區sendx位置。

然後,將sendx移到下一個位置。如果sendx到達最後一個位置,則將其設為0,這是典型的首尾相連的方法。

緩衝區讀取

當緩衝區未滿時,sendq也必須為空(因為如果緩衝區未滿,發送資料的Goroutine不會排隊,而是直接將資料放入緩衝區)。此時Channel的讀取邏輯chanrecv比較簡單,可以直接從緩衝區讀取數據,也是一個移動recvx的過程,與上面的緩衝區寫入基本上相同。

sendq中有等待的Goroutine時,緩衝區此時一定已滿。此時Channel的讀取邏輯如下:

<code class="language-go">func makechan(t *chantype, size int) *hchan</code>

ep是接收資料的變數對應的位址(例如,在x := <-c中,ep就是x的位址)。 sg表示從sendq取出的第一個sudog。程式碼中:

  • typedmemmove(c.elemtype, ep, qp)表示將緩衝區中目前可讀的元素複製到接收變數的位址。
  • typedmemmove(c.elemtype, qp, sg.elem)表示將sendq中Goroutine等待發送的資料複製到緩衝區。因為後面執行了recv ,所以相當於將sendq中的資料放在佇列的末端。

簡單來說,這裡Channel將緩衝區中的第一個資料複製到對應的接收變量,同時將sendq中的元素複製到佇列的末尾,從而實現FIFO(先進先出)。

總結

Channel作為Golang中最常用的設施之一,理解其原始碼有助於更好地使用和理解Channel。同時,也不要過度迷信和依賴Channel的效能,目前Channel的設計仍有很大的優化空間。

最佳化建議:

  • 使用更輕量級的鎖定機製或無鎖定方案,以提高效能。
  • 最佳化緩衝區管理,減少記憶體分配和複製操作。

Leapcell:Golang Web 應用最佳 Serverless 平台

Go Channel Unlocked: How They Work

最後,推薦一個非常適合部署Go服務的平台:Leapcell

  1. 多語言支援: 支援JavaScript、Python、Go或Rust開發。
  2. 免費部署無限項目: 只按使用付費,無請求則無費用。
  3. 極高的性價比: 隨選付費,無空閒費用。例如:25美元可支援694萬次請求,平均回應時間為60毫秒。
  4. 流暢的開發者體驗: 直覺的UI,輕鬆設定;全自動CI/CD管道和GitOps整合;即時指標和日誌,提供可操作的洞察。
  5. 輕鬆擴展和高效能: 自動擴展以輕鬆處理高並發;零運營開銷,專注於構建。
Go Channel Unlocked: How They Work

更多資訊請查看文件!

Leapcell Twitter: https://www.php.cn/link/7884effb9452a6d7a7a79499ef854afd

以上是Go 頻道解鎖:它們是如何運作的的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
上一篇:OSD:我的第二選擇下一篇:暫無