欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

一文帶你了解Golang中select的實(shí)現(xiàn)原理

 更新時(shí)間:2023年02月19日 09:37:55   作者:nil  
select是go提供的一種跟并發(fā)相關(guān)的語法,非常有用。本文將介紹?Go?語言中的?select?的實(shí)現(xiàn)原理,包括?select?的結(jié)構(gòu)和常見問題、編譯期間的多種優(yōu)化以及運(yùn)行時(shí)的執(zhí)行過程

概述

select是go提供的一種跟并發(fā)相關(guān)的語法,非常有用。本文將介紹 Go 語言中的 select 的實(shí)現(xiàn)原理,包括 select 的結(jié)構(gòu)和常見問題、編譯期間的多種優(yōu)化以及運(yùn)行時(shí)的執(zhí)行過程。

select 是一種與 switch 非常相似的控制結(jié)構(gòu),與 switch 不同的是,select 中雖然也有多個(gè) case,但是這些 case 中的表達(dá)式都必須與 Channel 的操作有關(guān),也就是 Channel 的讀寫操作,下面的函數(shù)就展示了一個(gè)包含從 Channel 中讀取數(shù)據(jù)和向 Channel 發(fā)送數(shù)據(jù)的 select 結(jié)構(gòu):

func fibonacci(c, quit chan int) {
    x, y := 0, 1
    for {
        select {
        case c <- x:
            x, y = y, x+y
        case <-quit:
            fmt.Println("quit")
            return
        }
    }
}

這個(gè) select 控制結(jié)構(gòu)就會(huì)等待 c <- x 或者 <-quit 兩個(gè)表達(dá)式中任意一個(gè)的返回,無論哪一個(gè)返回都會(huì)立刻執(zhí)行 case 中的代碼,不過如果了 select 中的兩個(gè) case 同時(shí)被觸發(fā),就會(huì)隨機(jī)選擇一個(gè) case 執(zhí)行。

結(jié)構(gòu)

select 在 Go 語言的源代碼中其實(shí)不存在任何的結(jié)構(gòu)體表示,但是 select 控制結(jié)構(gòu)中 case 卻使用了 scase 結(jié)構(gòu)體來表示:

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

由于非 default 的 case 中都與 Channel 的發(fā)送和接收數(shù)據(jù)有關(guān),所以在 scase 結(jié)構(gòu)體中也包含一個(gè) c 字段用于存儲(chǔ) case 中使用的 Channel,elem 是用于接收或者發(fā)送數(shù)據(jù)的變量地址、kind 表示當(dāng)前 case 的種類,總共包含以下四種:

const (
    caseNil = iota
    caseRecv
    caseSend
    caseDefault
)

這四種常量分別表示不同類型的 case,相信它們的命名已經(jīng)能夠充分幫助我們理解它們的作用了,所以在這里也不再展開介紹了。

現(xiàn)象

當(dāng)我們?cè)?Go 語言中使用 select 控制結(jié)構(gòu)時(shí),其實(shí)會(huì)遇到兩個(gè)非常有趣的問題,一個(gè)是 select 能在 Channel 上進(jìn)行非阻塞的收發(fā)操作,另一個(gè)是 select 在遇到多個(gè) Channel 同時(shí)響應(yīng)時(shí)能夠隨機(jī)挑選 case 執(zhí)行。

非阻塞的收發(fā)

如果一個(gè) select 控制結(jié)構(gòu)中包含一個(gè) default 表達(dá)式,那么這個(gè) select 并不會(huì)等待其它的 Channel 準(zhǔn)備就緒,而是會(huì)非阻塞地讀取或者寫入數(shù)據(jù):

func main() {
    ch := make(chan int)
    select {
    case i := <-ch:
        println(i)

    default:
        println("default")
    }
}

$ go run main.go
default

當(dāng)我們運(yùn)行上面的代碼時(shí)其實(shí)也并不會(huì)阻塞當(dāng)前的 Goroutine,而是會(huì)直接執(zhí)行 default 條件中的內(nèi)容并返回。

隨機(jī)執(zhí)行

另一個(gè)使用 select 遇到的情況其實(shí)就是同時(shí)有多個(gè) case 就緒后,select 如何進(jìn)行選擇的問題,我們通過下面的代碼可以簡(jiǎn)單了解一下:

func main() {
    ch := make(chan int)
    go func() {
        for range time.Tick(1 * time.Second) {
            ch <- 0
        }
    }()

    for {
        select {
        case <-ch:
            println("case1")

        case <-ch:
            println("case2")
        }
    }
}

$ go run main.go
case1
case2
case1
case2
case2
case1
...

從上述代碼輸出的結(jié)果中我們可以看到,select 在遇到兩個(gè) <-ch 同時(shí)響應(yīng)時(shí)其實(shí)會(huì)隨機(jī)選擇一個(gè) case 執(zhí)行其中的表達(dá)式,我們會(huì)在這一節(jié)中介紹這一現(xiàn)象的實(shí)現(xiàn)原理。

編譯

select 語句在編譯期間會(huì)被轉(zhuǎn)換成 OSELECT 節(jié)點(diǎn),每一個(gè) OSELECT 節(jié)點(diǎn)都會(huì)持有一系列的 OCASE 節(jié)點(diǎn),如果 OCASE 節(jié)點(diǎn)的都是空的,就意味著這是一個(gè) default 節(jié)點(diǎn):

上圖展示的其實(shí)就是 select 在編譯期間的結(jié)構(gòu),每一個(gè) OCASE 既包含了執(zhí)行條件也包含了滿足條件后執(zhí)行的代碼,我們?cè)谶@一節(jié)中就會(huì)介紹 select 語句在編譯期間進(jìn)行的優(yōu)化和轉(zhuǎn)換。

編譯器在中間代碼生成期間會(huì)根據(jù) select 中 case 的不同對(duì)控制語句進(jìn)行優(yōu)化,這一過程其實(shí)都發(fā)生在 walkselectcases 函數(shù)中,我們?cè)谶@里會(huì)分四種情況分別介紹優(yōu)化的過程和結(jié)果:

select 中不存在任何的 case;

select 中只存在一個(gè) case;

select 中存在兩個(gè) case,其中一個(gè) case 是 default 語句;

通用的 select 條件;

我們會(huì)按照這四種不同的情況拆分 walkselectcases 函數(shù)并分別介紹不同場(chǎng)景下優(yōu)化的結(jié)果。

直接阻塞

首先介紹的其實(shí)就是最簡(jiǎn)單的情況,也就是當(dāng) select 結(jié)構(gòu)中不包含任何的 case 時(shí),編譯器是如何進(jìn)行處理的:

func walkselectcases(cases *Nodes) []*Node {
    n := cases.Len()

    if n == 0 {
        return []*Node{mkcall("block", nil, nil)}
    }
    // ...
}

這段代碼非常簡(jiǎn)單并且容易理解,它直接將類似 select {} 的空語句,轉(zhuǎn)換成對(duì) block 函數(shù)的調(diào)用:

func block() {
    gopark(nil, nil, waitReasonSelectNoCases, traceEvGoStop, 1)
}

block 函數(shù)的實(shí)現(xiàn)非常簡(jiǎn)單,它會(huì)運(yùn)行 gopark 讓出當(dāng)前 Goroutine 對(duì)處理器的使用權(quán),該 Goroutine 也會(huì)進(jìn)入永久休眠的狀態(tài)也沒有辦法被其他的 Goroutine 喚醒,我們可以看到調(diào)用 gopark 方法時(shí)傳入的等待原因是 waitReasonSelectNoCases,這其實(shí)也在告訴我們一個(gè)空的 select 語句會(huì)直接阻塞當(dāng)前的 Goroutine。

獨(dú)立情況

如果當(dāng)前的 select 條件只包含一個(gè) case,那么就會(huì)就會(huì)執(zhí)行如下的優(yōu)化策略將原來的 select 語句改寫成 if 條件語句,下面是在 select 中從 Channel 接受數(shù)據(jù)時(shí)被改寫的情況:

select {
case v, ok <-ch:
    // ...    
}

if ch == nil {
    block()
}
v, ok := <-ch
// ...

在 walkselectcases 函數(shù)中,如果只包含一個(gè)發(fā)送的 case,那么就不會(huì)包含 v, ok := <- ch 這個(gè)表達(dá)式,因?yàn)橄?Channel 發(fā)送數(shù)據(jù)并沒有任何的返回值。

我們可以看到如果在 select 中僅存在一個(gè) case,那么當(dāng) case 中處理的 Channel 是空指針時(shí),就會(huì)發(fā)生和沒有 case 的 select 語句一樣的情況,也就是直接掛起當(dāng)前 Goroutine 并且永遠(yuǎn)不會(huì)被喚醒。

非阻塞操作

在下一次的優(yōu)化策略執(zhí)行之前,walkselectcases 函數(shù)會(huì)先將 case 中所有 Channel 都轉(zhuǎn)換成指向 Channel 的地址以便于接下來的優(yōu)化和通用邏輯的執(zhí)行,改寫之后就會(huì)進(jìn)行最后一次的代碼優(yōu)化,觸發(fā)的條件就是 — select 中包含兩個(gè) case,但是其中一個(gè)是 default,我們可以分成發(fā)送和接收兩種情況介紹處理的過程。

發(fā)送

首先就是 Channel 的發(fā)送過程,也就是 case 中的表達(dá)式是 OSEND 類型,在這種情況下會(huì)使用 if/else 語句改寫代碼:

select {
case ch <- i:
    // ...
default:
    // ...
}

if selectnbsend(ch, i) {
    // ...
} else {
    // ...
}

這里最重要的函數(shù)其實(shí)就是 selectnbsend,它的主要作用就是非阻塞地向 Channel 中發(fā)送數(shù)據(jù),我們?cè)?Channel 一節(jié)曾經(jīng)提到過發(fā)送數(shù)據(jù)的 chansend 函數(shù)包含一個(gè) block 參數(shù),這個(gè)參數(shù)會(huì)決定這一次的發(fā)送是不是阻塞的:

func selectnbsend(c *hchan, elem unsafe.Pointer) (selected bool) {
    return chansend(c, elem, false, getcallerpc())
}

在這里我們只需要知道當(dāng)前的發(fā)送過程不是阻塞的,哪怕是沒有接收方、緩沖區(qū)空間不足導(dǎo)致失敗了也會(huì)立即返回。

接收

由于從 Channel 中接收數(shù)據(jù)可能會(huì)返回一個(gè)或者兩個(gè)值,所以這里的情況會(huì)比發(fā)送時(shí)稍顯復(fù)雜,不過改寫的套路和邏輯確是差不多的:

select {
case v <- ch: // case v, received <- ch:
    // ...
default:
    // ...
}

if selectnbrecv(&v, ch) { // if selectnbrecv2(&v, &received, ch) {
    // ...
} else {
    // ...
}

返回值數(shù)量不同會(huì)導(dǎo)致最終使用函數(shù)的不同,兩個(gè)用于非阻塞接收消息的函數(shù) selectnbrecv 和 selectnbrecv2 其實(shí)只是對(duì) chanrecv 返回值的處理稍有不同:

func selectnbrecv(elem unsafe.Pointer, c *hchan) (selected bool) {
    selected, _ = chanrecv(c, elem, false)
    return
}

func selectnbrecv2(elem unsafe.Pointer, received *bool, c *hchan) (selected bool) {
    selected, *received = chanrecv(c, elem, false)
    return
}

因?yàn)榻邮辗讲恍枰?,所?nbsp;selectnbrecv 會(huì)直接忽略返回的布爾值,而 selectnbrecv2 會(huì)將布爾值回傳給上層;與 chansend 一樣,chanrecv 也提供了一個(gè) block 參數(shù)用于控制這一次接收是否阻塞。

通用情況

在默認(rèn)的情況下,select 語句會(huì)在編譯階段經(jīng)過如下過程的處理:

  • 將所有的 case 轉(zhuǎn)換成包含 Channel 以及類型等信息的 scase 結(jié)構(gòu)體;
  • 調(diào)用運(yùn)行時(shí)函數(shù) selectgo 獲取被選擇的 scase 結(jié)構(gòu)體索引,如果當(dāng)前的 scase 是一個(gè)接收數(shù)據(jù)的操作,還會(huì)返回一個(gè)指示當(dāng)前 case 是否是接收的布爾值;
  • 通過 for 循環(huán)生成一組 if 語句,在語句中判斷自己是不是被選中的 case

一個(gè)包含三個(gè) case 的正常 select 語句其實(shí)會(huì)被展開成如下所示的邏輯,我們可以看到其中處理的三個(gè)部分:

selv := [3]scase{}
order := [6]uint16
for i, cas := range cases {
    c := scase{}
    c.kind = ...
    c.elem = ...
    c.c = ...
}
chosen, revcOK := selectgo(selv, order, 3)
if chosen == 0 {
    // ...
    break
}
if chosen == 1 {
    // ...
    break
}
if chosen == 2 {
    // ...
    break
}

展開后的 select 其實(shí)包含三部分,最開始初始化數(shù)組并轉(zhuǎn)換 scase 結(jié)構(gòu)體,使用 selectgo 選擇執(zhí)行的 case 以及最后通過 if 判斷選中的情況并執(zhí)行 case 中的表達(dá)式,需要注意的是這里其實(shí)也僅僅展開了 select 控制結(jié)構(gòu),select 語句執(zhí)行最重要的過程其實(shí)也是選擇 case 執(zhí)行的過程,這是我們?cè)谙乱还?jié)運(yùn)行時(shí)重點(diǎn)介紹的。

運(yùn)行時(shí)

我們已經(jīng)充分地了解了 select 在編譯期間的處理過程,接下來可以展開介紹 selectgo 函數(shù)的實(shí)現(xiàn)原理了。

func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool) { } selectgo 是會(huì)在運(yùn)行期間運(yùn)行的函數(shù),這個(gè)函數(shù)的主要作用就是從 select 控制結(jié)構(gòu)中的多個(gè) case 中選擇一個(gè)需要執(zhí)行的 case,隨后的多個(gè) if 條件語句就會(huì)根據(jù) selectgo 的返回值執(zhí)行相應(yīng)的語句。

初始化

selectgo 函數(shù)首先會(huì)進(jìn)行執(zhí)行必要的一些初始化操作,也就是決定處理 case 的兩個(gè)順序,其中一個(gè)是 pollOrder 另一個(gè)是 lockOrder

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)

    // ...
}

Channel 的輪詢順序是通過 fastrandn 隨機(jī)生成的,這其實(shí)就導(dǎo)致了如果多個(gè) Channel 同時(shí)『響應(yīng)』,select 會(huì)隨機(jī)選擇其中的一個(gè)執(zhí)行;而另一個(gè) lockOrder 就是根據(jù) Channel 的地址確定的,根據(jù)相同的順序鎖定 Channel 能夠避免死鎖的發(fā)生,最后調(diào)用的 sellock 就會(huì)按照之前生成的順序鎖定所有的 Channel。

循環(huán)

當(dāng)我們?yōu)?nbsp;select 語句確定了輪詢和鎖定的順序并鎖定了所有的 Channel 之后就會(huì)開始進(jìn)入 select 的主循環(huán),查找或者等待 Channel 準(zhǔn)備就緒,循環(huán)中會(huì)遍歷所有的 case 并找到需要被喚起的 sudog 結(jié)構(gòu)體,在這段循環(huán)的代碼中,我們會(huì)分四種不同的情況處理 select 中的多個(gè) case

caseNil — 當(dāng)前 case 不包含任何的 Channel,就直接會(huì)被跳過;

caseRecv — 當(dāng)前 case 會(huì)從 Channel 中接收數(shù)據(jù);

  • 如果當(dāng)前 Channel 的 sendq 上有等待的 Goroutine 就會(huì)直接跳到 recv 標(biāo)簽所在的代碼段,從 Goroutine 中獲取最新發(fā)送的數(shù)據(jù);
  • 如果當(dāng)前 Channel 的緩沖區(qū)不為空就會(huì)跳到 bufrecv 標(biāo)簽處從緩沖區(qū)中獲取數(shù)據(jù);
  • 如果當(dāng)前 Channel 已經(jīng)被關(guān)閉就會(huì)跳到 rclose 做一些清除的收尾工作;

caseSend — 當(dāng)前 case 會(huì)向 Channel 發(fā)送數(shù)據(jù);

  • 如果當(dāng)前 Channel 已經(jīng)被關(guān)閉就會(huì)直接跳到 rclose 代碼段;
  • 如果當(dāng)前 Channel 的 recvq 上有等待的 Goroutine 就會(huì)跳到 send 代碼段向 Channel 直接發(fā)送數(shù)據(jù);

caseDefault — 當(dāng)前 case 表示默認(rèn)情況,如果循環(huán)執(zhí)行到了這種情況就表示前面的所有 case 都沒有被執(zhí)行,所以這里會(huì)直接解鎖所有的 Channel 并退出 selectgo 函數(shù),這時(shí)也就意味著當(dāng)前 select 結(jié)構(gòu)中的其他收發(fā)語句都是非阻塞的。

這其實(shí)是循環(huán)執(zhí)行的第一次遍歷,主要作用就是尋找所有 case 中 Channel 是否有可以立刻被處理的情況,無論是在包含等待的 Goroutine 還是緩沖區(qū)中存在數(shù)據(jù),只要滿足條件就會(huì)立刻處理,如果不能立刻找到活躍的 Channel 就會(huì)進(jìn)入循環(huán)的下一個(gè)過程,按照需要將當(dāng)前的 Goroutine 加入到所有 Channel 的 sendq 或者 recvq 隊(duì)列中:

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)

    // ...
}

這里創(chuàng)建 sudog 并入隊(duì)的過程其實(shí)和 Channel 中直接進(jìn)行發(fā)送和接收時(shí)的過程幾乎完全相同,只是除了在入隊(duì)之外,這些 sudog 結(jié)構(gòu)體都會(huì)被串成鏈表附著在當(dāng)前 Goroutine 上,在入隊(duì)之后會(huì)調(diào)用 gopark 函數(shù)掛起當(dāng)前的 Goroutine 等待調(diào)度器的喚醒。

等到 select 對(duì)應(yīng)的一些 Channel 準(zhǔn)備好之后,當(dāng)前 Goroutine 就會(huì)被調(diào)度器喚醒,這時(shí)就會(huì)繼續(xù)執(zhí)行 selectgo 函數(shù)中剩下的邏輯,也就是從上面 入隊(duì)的 sudog 結(jié)構(gòu)體中獲取數(shù)據(jù):

func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool) {
    // ...
    gp.selectDone = 0
    sg = (*sudog)(gp.param)
    gp.param = nil

    casi = -1
    cas = nil
    sglist = gp.waiting
    gp.waiting = nil

    for _, casei := range lockorder {
        k = &scases[casei]
        if sg == sglist {
            casi = int(casei)
            cas = k
        } else {
            if k.kind == caseSend {
                c.sendq.dequeueSudoG(sglist)
            } else {
                c.recvq.dequeueSudoG(sglist)
            }
        }
        sgnext = sglist.waitlink
        sglist.waitlink = nil
        releaseSudog(sglist)
        sglist = sgnext
    }

    c = cas.c

    if cas.kind == caseRecv {
        recvOK = true
    }

    selunlock(scases, lockorder)
    goto retc
    // ...
}

在第三次根據(jù) lockOrder 遍歷全部 case 的過程中,我們會(huì)先獲取 Goroutine 接收到的參數(shù) param,這個(gè)參數(shù)其實(shí)就是被喚醒的 sudog 結(jié)構(gòu),我們會(huì)依次對(duì)比所有 case 對(duì)應(yīng)的 sudog 結(jié)構(gòu)找到被喚醒的 case 并釋放其他未被使用的 sudog 結(jié)構(gòu)。

由于當(dāng)前的 select 結(jié)構(gòu)已經(jīng)挑選了其中的一個(gè) case 進(jìn)行執(zhí)行,那么剩下 case 中沒有被用到的 sudog 其實(shí)就會(huì)直接忽略并且釋放掉了,為了不影響 Channel 的正常使用,我們還是需要將這些廢棄的 sudog 從 Channel 中出隊(duì);而除此之外的發(fā)生事件導(dǎo)致我們被喚醒的 sudog 結(jié)構(gòu)已經(jīng)在 Channel 進(jìn)行收發(fā)時(shí)就已經(jīng)出隊(duì)了,不需要我們?cè)俅翁幚恚鲫?duì)的代碼以及相關(guān)分析其實(shí)都在 Channel 一節(jié)中發(fā)送和接收的章節(jié)。

當(dāng)我們?cè)谘h(huán)中發(fā)現(xiàn)緩沖區(qū)中有元素或者緩沖區(qū)未滿時(shí)就會(huì)通過 goto 關(guān)鍵字跳轉(zhuǎn)到以下的兩個(gè)代碼段,這兩段代碼的執(zhí)行過程其實(shí)都非常簡(jiǎn)單,都只是向 Channel 中發(fā)送或者從緩沖區(qū)中直接獲取新的數(shù)據(jù):

bufrecv:
    recvOK = true
    qp = chanbuf(c, c.recvx)
    if cas.elem != nil {
        typedmemmove(c.elemtype, cas.elem, qp)
    }
    typedmemclr(c.elemtype, qp)
    c.recvx++
    if c.recvx == c.dataqsiz {
        c.recvx = 0
    }
    c.qcount--
    selunlock(scases, lockorder)
    goto retc

bufsend:
    typedmemmove(c.elemtype, chanbuf(c, c.sendx), cas.elem)
    c.sendx++
    if c.sendx == c.dataqsiz {
        c.sendx = 0
    }
    c.qcount++
    selunlock(scases, lockorder)
    goto retc

這里在緩沖區(qū)中進(jìn)行的操作和直接對(duì) Channel 調(diào)用 chansend 和 chanrecv 進(jìn)行收發(fā)的過程差不多,執(zhí)行結(jié)束之后就會(huì)直接跳到 retc 字段。

兩個(gè)直接收發(fā)的情況,其實(shí)也就是調(diào)用 Channel 運(yùn)行時(shí)的兩個(gè)方法 send 和 recv,這兩個(gè)方法會(huì)直接操作對(duì)應(yīng)的 Channel:

recv:
    recv(c, sg, cas.elem, func() { selunlock(scases, lockorder) }, 2)
    recvOK = true
    goto retc

send:
    send(c, sg, cas.elem, func() { selunlock(scases, lockorder) }, 2)
    goto retc

不過當(dāng)發(fā)送或者接收時(shí),情況就稍微有一點(diǎn)復(fù)雜了,從一個(gè)關(guān)閉 Channel 中接收數(shù)據(jù)會(huì)直接清除 Channel 中的相關(guān)內(nèi)容,而向一個(gè)關(guān)閉的 Channel 發(fā)送數(shù)據(jù)就會(huì)直接 panic 造成程序崩潰:

rclose:
    selunlock(scases, lockorder)
    recvOK = false
    if cas.elem != nil {
        typedmemclr(c.elemtype, cas.elem)
    }
    goto retc

sclose:
    selunlock(scases, lockorder)
    panic(plainError("send on closed channel"))

總體來看,Channel 相關(guān)的收發(fā)操作和上一節(jié) Channel 實(shí)現(xiàn)原理中介紹的沒有太多出入,只是由于 select 多出了 default 關(guān)鍵字所以會(huì)出現(xiàn)非阻塞收發(fā)的情況。

總結(jié)

到這一節(jié)的最后我們需要總結(jié)一下,select 結(jié)構(gòu)的執(zhí)行過程與實(shí)現(xiàn)原理,首先在編譯期間,Go 語言會(huì)對(duì) select 語句進(jìn)行優(yōu)化,以下是根據(jù) select 中語句的不同選擇了不同的優(yōu)化路徑:

空的 select 語句會(huì)被直接轉(zhuǎn)換成 block 函數(shù)的調(diào)用,直接掛起當(dāng)前 Goroutine;

如果 select 語句中只包含一個(gè) case,就會(huì)被轉(zhuǎn)換成 if ch == nil { block }; n; 表達(dá)式;

  • 首先判斷操作的 Channel 是不是空的;
  • 然后執(zhí)行 case 結(jié)構(gòu)中的內(nèi)容;

如果 select 語句中只包含兩個(gè) case 并且其中一個(gè)是 default,那么 Channel 和接收和發(fā)送操作都會(huì)使用 selectnbrecv 和 selectnbsend 非阻塞地執(zhí)行接收和發(fā)送操作;

在默認(rèn)情況下會(huì)通過 selectgo 函數(shù)選擇需要執(zhí)行的 case 并通過多個(gè) if 語句執(zhí)行 case 中的表達(dá)式;

在編譯器已經(jīng)對(duì) select 語句進(jìn)行優(yōu)化之后,Go 語言會(huì)在運(yùn)行時(shí)執(zhí)行編譯期間展開的 selectgo 函數(shù),這個(gè)函數(shù)會(huì)按照以下的過程執(zhí)行:

1.隨機(jī)生成一個(gè)遍歷的輪詢順序 pollOrder 并根據(jù) Channel 地址生成一個(gè)用于遍歷的鎖定順序 lockOrder

2.根據(jù) pollOrder 遍歷所有的 case 查看是否有可以立刻處理的 Channel 消息;

  • 如果有消息就直接獲取 case 對(duì)應(yīng)的索引并返回;
  • 如果沒有消息就會(huì)創(chuàng)建 sudog 結(jié)構(gòu)體,將當(dāng)前 Goroutine 加入到所有相關(guān) Channel 的 sendq 和 recvq 隊(duì)列中并調(diào)用 gopark 觸發(fā)調(diào)度器的調(diào)度;

3.當(dāng)調(diào)度器喚醒當(dāng)前 Goroutine 時(shí)就會(huì)再次按照 lockOrder 遍歷所有的 case,從中查找需要被處理的 sudog 結(jié)構(gòu)并返回對(duì)應(yīng)的索引;

然而并不是所有的 select 控制結(jié)構(gòu)都會(huì)走到 selectgo 上,很多情況都會(huì)被直接優(yōu)化掉,沒有機(jī)會(huì)調(diào)用 selectgo 函數(shù)。

Go 語言中的 select 關(guān)鍵字與 IO 多路復(fù)用中的 selectepoll 等函數(shù)非常相似,不但 Channel 的收發(fā)操作與等待 IO 的讀寫能找到這種一一對(duì)應(yīng)的關(guān)系,這兩者的作用也非常相似;總的來說,select 關(guān)鍵字的實(shí)現(xiàn)原理稍顯復(fù)雜,與 Channel 的關(guān)系非常緊密,這里省略了很多 Channel 操作的細(xì)節(jié),數(shù)據(jù)結(jié)構(gòu)一章其實(shí)就介紹了 Channel 收發(fā)的相關(guān)細(xì)節(jié)。

到此這篇關(guān)于一文帶你了解Golang中select的實(shí)現(xiàn)原理的文章就介紹到這了,更多相關(guān)Golang select內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • Go語言Elasticsearch數(shù)據(jù)清理工具思路詳解

    Go語言Elasticsearch數(shù)據(jù)清理工具思路詳解

    這篇文章主要介紹了Go語言Elasticsearch數(shù)據(jù)清理工具思路詳解,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2021-10-10
  • 夯實(shí)Golang基礎(chǔ)之?dāng)?shù)據(jù)類型梳理匯總

    夯實(shí)Golang基礎(chǔ)之?dāng)?shù)據(jù)類型梳理匯總

    這篇文章主要8為大家介紹了夯實(shí)Golang基礎(chǔ)之?dāng)?shù)據(jù)類型梳理匯總,有需要的朋友可以借鑒參考下,希望能夠有所幫助
    2023-10-10
  • 淺談beego默認(rèn)處理靜態(tài)文件性能低下的問題

    淺談beego默認(rèn)處理靜態(tài)文件性能低下的問題

    下面小編就為大家?guī)硪黄獪\談beego默認(rèn)處理靜態(tài)文件性能低下的問題。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧
    2017-06-06
  • Go處理PDF的實(shí)現(xiàn)代碼

    Go處理PDF的實(shí)現(xiàn)代碼

    這篇文章主要介紹了Go處理PDF的實(shí)現(xiàn)代碼,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2020-01-01
  • Go+Kafka實(shí)現(xiàn)延遲消息的實(shí)現(xiàn)示例

    Go+Kafka實(shí)現(xiàn)延遲消息的實(shí)現(xiàn)示例

    本文主要介紹了Go+Kafka實(shí)現(xiàn)延遲消息的實(shí)現(xiàn)示例,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2022-07-07
  • Go方法簡(jiǎn)單性和高效性的充分體現(xiàn)詳解

    Go方法簡(jiǎn)單性和高效性的充分體現(xiàn)詳解

    本文深入探討了Go語言中方法的各個(gè)方面,包括基礎(chǔ)概念、定義與聲明、特性、實(shí)戰(zhàn)應(yīng)用以及性能考量,文章充滿技術(shù)深度,通過實(shí)例和代碼演示,力圖幫助讀者全面理解Go方法的設(shè)計(jì)哲學(xué)和最佳實(shí)踐
    2023-10-10
  • Go Module依賴管理的實(shí)現(xiàn)

    Go Module依賴管理的實(shí)現(xiàn)

    Go Module是Go語言的官方依賴管理解決方案,其提供了一種簡(jiǎn)單、可靠的方式來管理項(xiàng)目的依賴關(guān)系,本文主要介紹了Go Module依賴管理的實(shí)現(xiàn),感興趣的可以了解一下
    2024-06-06
  • Golang語言中的Prometheus的日志模塊使用案例代碼編寫

    Golang語言中的Prometheus的日志模塊使用案例代碼編寫

    這篇文章主要介紹了Golang語言中的Prometheus的日志模塊使用案例,本文給大家分享源代碼編寫方法,感興趣的朋友跟隨小編一起看看吧
    2024-08-08
  • go語言開發(fā)環(huán)境安裝及第一個(gè)go程序(推薦)

    go語言開發(fā)環(huán)境安裝及第一個(gè)go程序(推薦)

    這篇文章主要介紹了go語言開發(fā)環(huán)境安裝及第一個(gè)go程序,這篇通過實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2020-02-02
  • Go使用Protocol?Buffers在數(shù)據(jù)序列化的優(yōu)勢(shì)示例詳解

    Go使用Protocol?Buffers在數(shù)據(jù)序列化的優(yōu)勢(shì)示例詳解

    這篇文章主要為大家介紹了Go使用Protocol?Buffers在數(shù)據(jù)序列化的優(yōu)勢(shì)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪
    2023-11-11

最新評(píng)論