日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

深入淺出Golang中select的實現原理_Golang

作者:yi個俗人 ? 更新時間: 2022-10-12 編程語言

概述

在go語言中,select語句就是用來監聽和channel有關的IO操作,當IO操作發生時,觸發相應的case操作,有了select語句,可以實現main主線程與goroutine線程之間的互動。需要的朋友可以參考以下內容,希望對大家有幫助。

select實現原理

Golang實現select時,定義了一個數據結構表示每個case語句(包含defaultdefault實際上是一種特殊的case),select執行過程可以看成一個函數,函數輸入case數組,輸出選中的case,然后程序流程轉到選中的case塊。

執行流程

在默認的情況下,select?語句會在編譯階段經過如下過程的處理:

  • 將所有的?case?轉換成包含 Channel 以及類型等信息的?scase?結構體;
  • 調用運行時函數?selectgo?獲取被選擇的scase?結構體索引,如果當前的?scase?是一個接收數據的操作,還會返回一個指示當前case?是否是接收的布爾值;
  • 通過?for?循環生成一組?if?語句,在語句中判斷自己是不是被選中的?case

case數據結構

select控制結構中case使用了scase結構體來表示,源碼包src/runtime/select.go:scase定義了表示case語句的數據結構:

type scase struct {
    c           *hchan
    elem        unsafe.Pointer
    kind        uint16
    pc          uintptr
    releasetime int64
}

scase.c:由于非defaultcase中都與channel的發送和接收數據有關,所以在scase結構體中也包含一個c字段用于存儲case中使用的channel,為當前case語句所操作的channel指針,這也說明了一個case語句只能操作一個channel

scase.kind:表示該case的類型,分為讀channel寫channeldefault,三種類型分別由常量定義:

const (
    caseNil = iota
    caseRecv	//case語句中嘗試讀取scase.c中的數據;
    caseSend	//case語句中嘗試向scase.c中寫入數據;
    caseDefault //default語句
)

scase.elem:用于接收或者發送數據的變量地址,根據scase.kind不同,有不同的用途:

  • scase.kind == caseRecv : scase.elem表示讀出channel的數據存放地址;
  • scase.kind == caseSend : scase.elem表示將要寫入channel的數據存放地址;

執行select

在運行期間會調用selectgo()函數,這個函數主要作用是從select控制結構中的多個case中選擇一個需要執行的case,隨后的多個?if?條件語句就會根據?selectgo()?的返回值執行相應的語句。

運行時源碼包src/runtime/select.go:selectgo()定義了select選擇case的函數:

func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool) {
    cas1 := (*[1 << 16]scase)(unsafe.Pointer(cas0))
    order1 := (*[1 << 17]uint16)(unsafe.Pointer(order0))

    scases := cas1[:ncases:ncases]
    pollorder := order1[:ncases:ncases]
    lockorder := order1[ncases:][:ncases:ncases]

    for i := range scases {
        cas := &scases[i]
        if cas.c == nil && cas.kind != caseDefault {
            *cas = scase{}
        }
    }

    for i := 1; i < ncases; i++ {
        j := fastrandn(uint32(i + 1))
        pollorder[i] = pollorder[j]
        pollorder[j] = uint16(i)
    }

    // sort the cases by Hchan address to get the locking order.
    // ...
    
    sellock(scases, lockorder)

    // ...
}

selectgo?函數首先會進行執行一些初始化操作,也就是決定處理?case?的兩個順序,其中一個是?pollOrder?另一個是?lockOrder

函數參數:

  • cas0:為scase數組的首地址,selectgo()就是從這些scase中找出一個返回。
  • order0:為一個兩倍cas0數組長度的buffer,保存scase隨機序列pollorderscasechannel地址序列lockorder
  • pollorder:每次selectgo執行都會把scase序列打亂,以達到隨機檢測case的目的。
  • lockorder:所有case語句中channel序列,以達到去重防止對channel加鎖時重復加鎖的目的。
  • ncases:表示scase數組的長度

函數返回值:

  • int: 選中case的編號,這個case編號跟代碼一致
  • bool: 是否成功從channle中讀取了數據,如果選中的case是從channel中讀數據,則該返回值表示是否讀取成功。

循環

當?select?語句確定了輪詢和鎖定的順序并鎖定了所有的 Channel 之后就會開始進入?select的主循環,查找或者等待 Channel 準備就緒,循環中會遍歷所有的?case?并找到需要被喚起的sudog?結構體。

func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool) {
    // ...
    gp = getg()
    nextp = &gp.waiting
    for _, casei := range lockorder {
        casi = int(casei)
        cas = &scases[casi]
        if cas.kind == caseNil {
            continue
        }
        c = cas.c
        sg := acquireSudog()
        sg.g = gp
        sg.isSelect = true
        sg.elem = cas.elem
        sg.c = c
        *nextp = sg
        nextp = &sg.waitlink

        switch cas.kind {
        case caseRecv:
            c.recvq.enqueue(sg)

        case caseSend:
            c.sendq.enqueue(sg)
        }
    }

    gp.param = nil
    gopark(selparkcommit, nil, waitReasonSelect, traceEvGoBlockSelect, 1)

    // ...
}

在這段循環的代碼中,我們會分四種不同的情況處理?select?中的多個?case

caseNil?— 當前?case?不包含任何的 Channel,就直接會被跳過;

caseRecv?— 當前?case?會從 Channel 中接收數據;

  • 如果當前 Channel 的?sendq?上有等待的 Goroutine 就會直接跳到?recv?標簽所在的代碼段,從 Goroutine 中獲取最新發送的數據;
  • 如果當前 Channel 的緩沖區不為空就會跳到?bufrecv?標簽處從緩沖區中獲取數據;
  • 如果當前 Channel 已經被關閉就會跳到rclose?做一些清除的收尾工作;

caseSend?— 當前?case?會向 Channel 發送數據;

  • 如果當前 Channel 已經被關閉就會直接跳到?rclose?代碼段;
  • 如果當前 Channel 的?recvq?上有等待的 Goroutine 就會跳到?send?代碼段向 Channel 直接發送數據;

caseDefault?— 表示默認情況,如果循環執行到了這種情況就表示前面的所有case?都沒有被執行,所以這里會直接解鎖所有的 Channel 并退出?selectgo?函數,這時也就意味著當前?select?結構中的其他收發語句都是非阻塞的。

總結

通過以上內容我們簡單的了解了select結構的執行過程與實現原理,首先在編譯期間,Go 語言會對?select?語句進行優化, 對于空的select語句會直接轉換成block函數的調用,直接掛起當前Goroutine,如果select語句中只包含一個case,就會被轉換成if ch == nil {block}; n; 表達式。然后執行case結構體中內容。

在運行時會執行selectgo函數,隨機生成一個遍歷的輪詢順序pollOrder并根據Channel地址生成一個用于遍歷的鎖定順序lockOrder;然后根據pollOrder遍歷所有的case查看是否有可以處理的Channel消息,如果有消息就直接獲取case對應的索引并返回。如果沒有消息就會創建sudog結構體,將當前 Goroutine 加入到所有相關 Channel sendq??recvq?隊列中并調用?gopark?觸發調度器的調度;

注意: 并不是所有的select控制結構都會走到selectgo,很多情況都會被直接優化調。

原文鏈接:https://juejin.cn/post/7132355757944619039

欄目分類
最近更新