golang select詳解

xuefeng發表於2021-07-01

golang select 詳解

前言

select 是golang用來做channel多路複用的一種技術,和switch的語法很像,不過每個case只可以有一個channel,send 操作和 receive 操作都使用 “<-” 操作符,在 send 語句中,channel 和值分別在操作符左右兩邊,在 receive 語句中,操作符放在 channel 運算元的前面。
示例:

    c0 := make(chan struct{})
    c1 := make(chan int, 100)
    for {
        select {
        case <-c0:
            return
        case <-c1:
            return
        }
    }

select與channel

之前channel詳解文章中講到過channel的阻塞寫、阻塞讀、非阻塞寫、非阻塞讀,這裡不再贅述,需要說明的是,select不止用來做channel的非阻塞操作,主要是用來作為多路複用操作channel的,機制和linux的select很像
不同的寫法會觸發不同的機制,下面我們看看示例

// 阻塞讀,對應channel的 chanrecv1函式
select {
case <-c0:
    return
}

// 非阻塞讀,對應channel的 selectnbrecv 函式
select {
case <-c0:
    return
default:
    return
}

// 多路複用
select {
case <-c0:
    return
case <-c1:
    return
default:
    return
}

從上面的程式碼中可以看出select的三種機制
1:只有一個case,並且沒有default,相當於 <- c0的寫法,阻塞讀寫資料
2:一個case,一個default,就會直接對應channel的非阻塞讀寫資料
3:有多個case,對應了真正的select多路複用機制,case隨機執行,原始碼位於runtime/select.go
今天我們主要來討論一下第三種機制

資料結構

const (
    caseNil = iota
    caseRecv
    caseSend
    caseDefault
)

type scase struct {
    c    *hchan         // channel
    elem unsafe.Pointer // 傳送或者接受資料的變數地址
    kind uint16         // case型別, 對應上方常量
    //...
}

由於非 defaultcase 中都與 Channel 的傳送和接收資料有關,所以在 scase 結構體中也包含一個 c 欄位用於儲存 case 中使用的 Channel,elem 是用於接收或者傳送資料的變數地址、kind 表示當前 case 的種類

執行時

程式碼執行流程:/reflect/value.go/Select -> /runtime/select.go/reflect_rselect -> /runtime/select.go/selectgo

這裡主要講一下select下的兩個函式
reflect_rselect:

func reflect_rselect(cases []runtimeSelect) (int, bool) {
    //判斷case數量
    if len(cases) == 0 {
        block()
    }

    //構建case陣列
    sel := make([]scase, len(cases))

    //二倍的case長度 uint16陣列
    order := make([]uint16, 2*len(cases))

    //組裝case陣列
    for i := range cases {
        rc := &cases[i]
        switch rc.dir {
        case selectDefault:
            sel[i] = scase{kind: caseDefault}
        case selectSend:
            sel[i] = scase{kind: caseSend, c: rc.ch, elem: rc.val}
        case selectRecv:
            sel[i] = scase{kind: caseRecv, c: rc.ch, elem: rc.val}
        }
        if raceenabled || msanenabled {
            selectsetpc(&sel[i])
        }
    }

    return selectgo(&sel[0], &order[0], len(cases))
}

從上面程式碼註釋可以看出來,這個函式主要是為了組裝case陣列,每個元素就是一個scase結構

下面是本章的重點,selectgo函式,我們先了解一下selectgo函式裡都做了些什麼事
1、打亂陣列順序(隨機獲取case)
2、鎖定所有channel
3、遍歷所有channel,判斷是否有可讀或者可寫的,如果有,解鎖channel,返回對應資料
4、否則,判斷有沒有default,如果有,解鎖channel,返回default對應scase
5、否則,把當前groutian新增到所有channel的等待佇列裡,解鎖所有channel,等待被喚醒
6、被喚醒後,再次鎖定所有channel
7、遍歷channel,把g從channel等待佇列中移除,並找到喚醒goroutian的channel
8、如果對應的scase不為空,直接返回對應的值
9、否則迴圈此過程

程式碼解析:

func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool) {
    //...
    // 使用fastrandn隨機演算法,設定pollorder陣列,後面會根據這個陣列進行迴圈,以達到隨機case
    for i := 1; i < ncases; i++ {
        j := fastrandn(uint32(i + 1))
        pollorder[i] = pollorder[j]
        pollorder[j] = uint16(i)
    }

    // 這段程式碼是對lockorder做一個堆排序
    // 所有的goroutian進來lockorder都是相同排序
    // 防止不同順序的case進來時鎖定channel導致死鎖
    for i := 0; i < ncases; i++ {
        j := i
        // Start with the pollorder to permute cases on the same channel.
        c := scases[pollorder[i]].c
        for j > 0 && scases[lockorder[(j-1)/2]].c.sortkey() < c.sortkey() {
            k := (j - 1) / 2
            lockorder[j] = lockorder[k]
            j = k
        }
        lockorder[j] = pollorder[i]
    }
    for i := ncases - 1; i >= 0; i-- {
        o := lockorder[i]
        c := scases[o].c
        lockorder[i] = lockorder[0]
        j := 0
        for {
            k := j*2 + 1
            if k >= i {
                break
            }
            if k+1 < i && scases[lockorder[k]].c.sortkey() < scases[lockorder[k+1]].c.sortkey() {
                k++
            }
            if c.sortkey() < scases[lockorder[k]].c.sortkey() {
                lockorder[j] = lockorder[k]
                j = k
                continue
            }
            break
        }
        lockorder[j] = o
    }

    //根據lockorder的順序
    sellock(scases, lockorder)

    var (
        gp     *g
        sg     *sudog
        c      *hchan
        k      *scase
        sglist *sudog
        sgnext *sudog
        qp     unsafe.Pointer
        nextp  **sudog
    )

loop:
    // pass 1 - look for something already waiting
    var dfli int
    var dfl *scase
    var casi int
    var cas *scase
    var recvOK bool
    //迴圈所有case
    for i := 0; i < ncases; i++ {
        //根據pollorder找到scases陣列下標
        casi = int(pollorder[i])
        cas = &scases[casi]
        c = cas.c

        switch cas.kind {
        //如果kind為0,直接continue
        case caseNil:
            continue
        //如果kind為1,代表是接收
        case caseRecv:
            //從channel的傳送佇列中獲取groutian,如果有,跳到recv程式碼塊
            sg = c.sendq.dequeue()
            if sg != nil {
                goto recv
            }

            //判斷channel是否為帶緩衝的,並且緩衝區有值,跳到bufrecv程式碼塊
            if c.qcount > 0 {
                goto bufrecv
            }

            //如果channel已經關閉,跳到rclose程式碼塊
            if c.closed != 0 {
                goto rclose
            }

        //如果kind為2,代表是傳送
        case caseSend:
            //send時先判斷是否關閉
            //如果channel已經關閉,跳到sclose程式碼塊
            if c.closed != 0 {
                goto sclose
            }
            //如果channel的讀取佇列裡存在groutian,跳到send程式碼塊
            sg = c.recvq.dequeue()
            if sg != nil {
                goto send
            }
            //如果channel為緩衝型,並且資料沒滿,跳轉到bufsend程式碼塊
            if c.qcount < c.dataqsiz {
                goto bufsend
            }
        //如果kind為3,執行default邏輯
        case caseDefault:
            dfli = casi
            dfl = cas
        }
    }

    //程式碼能走到這裡,說明所有的channel都不具備讀取的時機,判斷是否有default
    //如果存在default,先解鎖所有channel,跳轉到retc程式碼塊
    if dfl != nil {
        selunlock(scases, lockorder)
        casi = dfli
        cas = dfl
        goto retc
    }

    // 建立一個goroutian結構
    gp = getg()
    if gp.waiting != nil {
        throw("gp.waiting != nil")
    }

    //迴圈scases,把groutian儲存到channel對應的讀寫佇列中
    //設定gp.waiting為sudog連結串列頭結點
    nextp = &gp.waiting
    for _, casei := range lockorder {
        casi = int(casei)
        cas = &scases[casi]
        if cas.kind == caseNil {
            continue
        }
        c = cas.c
        //構建sudog
        sg := acquireSudog()
        sg.g = gp
        sg.isSelect = true
        // No stack splits between assigning elem and enqueuing
        // sg on gp.waiting where copystack can find it.
        sg.elem = cas.elem
        sg.releasetime = 0
        if t0 != 0 {
            sg.releasetime = -1
        }
        sg.c = c
        //gp.waiting佇列新增資料
        *nextp = sg
        nextp = &sg.waitlink
        //如果kind為1,儲存在channel的接收佇列中
        switch cas.kind {
        case caseRecv:
            c.recvq.enqueue(sg)
        //如果kind為2,儲存在channel的傳送佇列中
        case caseSend:
            c.sendq.enqueue(sg)
        }
    }

    // wait for someone to wake us up
    //設定goroutian的回撥,如果有channel喚醒goroutian,會把對應的sudog儲存到param中
    gp.param = nil

    //掛起goroutian,selparkcommit會給所有channel解鎖
    gopark(selparkcommit, nil, waitReasonSelect, traceEvGoBlockSelect, 1)
    gp.activeStackChans = false

    //喚醒後先給channel加鎖
    sellock(scases, lockorder)
    gp.selectDone = 0

    //喚醒groutian對應的sudog
    sg = (*sudog)(gp.param)
    gp.param = nil

    // pass 3 - dequeue from unsuccessful chans
    // otherwise they stack up on quiet channels
    // record the successful case, if any.
    // We singly-linked up the SudoGs in lock order.
    casi = -1
    cas = nil

    //sglist為sudog連結串列頭結點
    sglist = gp.waiting
    // Clear all elem before unlinking from gp.waiting.
    for sg1 := gp.waiting; sg1 != nil; sg1 = sg1.waitlink {
        sg1.isSelect = false
        sg1.elem = nil
        sg1.c = nil
    }
    gp.waiting = nil

    //迴圈所有case
    for _, casei := range lockorder {
        k = &scases[casei]
        if k.kind == caseNil {
            continue
        }
        if sglist.releasetime > 0 {
            k.releasetime = sglist.releasetime
        }
        //找到喚醒goroutian的sudog
        if sg == sglist {
            // sg has already been dequeued by the G that woke us up.
            casi = int(casei)
            cas = k
        } else { //從對應的讀寫佇列中刪除sudog
            c = k.c
            if k.kind == caseSend {
                c.sendq.dequeueSudoG(sglist)
            } else {
                c.recvq.dequeueSudoG(sglist)
            }
        }
        //從連結串列中獲取下一個sudog,繼續迴圈、刪除讀寫佇列
        sgnext = sglist.waitlink
        sglist.waitlink = nil
        releaseSudog(sglist)
        sglist = sgnext
    }

    if cas == nil {
        //如果喚醒的case為nil,從loop重新開始
        goto loop
    }

    c = cas.c

    if debugSelect {
        print("wait-return: cas0=", cas0, " c=", c, " cas=", cas, " kind=", cas.kind, "\n")
    }

    //如果是case是接收
    if cas.kind == caseRecv {
        recvOK = true
    }
    //繼續鎖定channel
    selunlock(scases, lockorder)

    //跳轉到retc程式碼塊
    goto retc

    //channel的緩衝區有資料時,直接從緩衝區獲取資料
bufrecv:
    recvOK = true
    qp = chanbuf(c, c.recvx)
    //如果有接收值,把資料地址存入elem中
    if cas.elem != nil {
        typedmemmove(c.elemtype, cas.elem, qp)
    }
    typedmemclr(c.elemtype, qp)
    //接收索引往後挪一位或者初始化為0
    c.recvx++
    if c.recvx == c.dataqsiz {
        c.recvx = 0
    }
    //緩衝區的資料量減少一個
    c.qcount--

    //解鎖所有channel
    selunlock(scases, lockorder)

    //跳轉到retc程式碼塊
    goto retc

    //channel的緩衝區有空閒位置時,把資料直接寫入buffer中
bufsend:
    //設定資料到緩衝區
    typedmemmove(c.elemtype, chanbuf(c, c.sendx), cas.elem)

    //傳送下標向後挪動或者初始化為0
    c.sendx++
    if c.sendx == c.dataqsiz {
        c.sendx = 0
    }

    //緩衝區中資料量加1
    c.qcount++
    //解鎖channel
    selunlock(scases, lockorder)
    //跳轉到retc程式碼區
    goto retc

    //如果傳送佇列中有groutian
recv:
    // can receive from sleeping sender (sg)
    // 從傳送的sudog中獲取資料
    // 解鎖channel
    // 喚醒goroutian
    recv(c, sg, cas.elem, func() { selunlock(scases, lockorder) }, 2)
    if debugSelect {
        print("syncrecv: cas0=", cas0, " c=", c, "\n")
    }
    recvOK = true
    goto retc

    //接收時channel已關閉
rclose:
    // read at end of closed channel
    // 解鎖channel
    selunlock(scases, lockorder)
    recvOK = false

    // 如果有有接收值, eg: case a := <- chan0,把資料地址賦值給elem
    if cas.elem != nil {
        typedmemclr(c.elemtype, cas.elem)
    }
    if raceenabled {
        raceacquire(c.raceaddr())
    }
    goto retc

    //傳送時channel中存在接收goroutian
send:
    //把資料傳送到接收的goroutian中
    //解鎖channel
    //喚醒goroutian
    send(c, sg, cas.elem, func() { selunlock(scases, lockorder) }, 2)
    if debugSelect {
        print("syncsend: cas0=", cas0, " c=", c, "\n")
    }
    goto retc

    //返回
retc:
    if cas.releasetime > 0 {
        blockevent(cas.releasetime-t0, 1)
    }
    //返回對應case的下標,如果是接收,返回recvOK,channel關閉時為false
    return casi, recvOK

    //傳送時channel已關閉,解鎖channel,直接panic
sclose:
    // send on closed channel
    selunlock(scases, lockorder)
    panic(plainError("send on closed channel"))
}

recv接收方法

func recv(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
    //如果為非緩衝區
    if c.dataqsiz == 0 {
        //...
        if ep != nil {
            // 從sender佇列中直接複製資料=
            recvDirect(c.elemtype, sg, ep)
        }
    } else {
        qp := chanbuf(c, c.recvx)
        //...
        //如果接受變數不為空,符合資料到ep
        if ep != nil {
            typedmemmove(c.elemtype, ep, qp)
        }
        //從緩衝區複製資料
        typedmemmove(c.elemtype, qp, sg.elem)
        c.recvx++
        if c.recvx == c.dataqsiz {
            c.recvx = 0
        }
        c.sendx = c.recvx // c.sendx = (c.sendx+1) % c.dataqsiz
    }
    sg.elem = nil
    gp := sg.g

    //解鎖channel
    unlockf()
    //傳送者的param設定
    gp.param = unsafe.Pointer(sg)
    if sg.releasetime != 0 {
        sg.releasetime = cputicks()
    }
    //喚醒goroutian
    goready(gp, skip+1)
}

send方法:

func send(c *hchan, sg *sudog, ep unsafe.Pointer, unlockf func(), skip int) {
    //...
    //傳送資料如果不為空
    if sg.elem != nil {
        //直接把資料寫入接收者goroutian
        sendDirect(c.elemtype, sg, ep)
        sg.elem = nil
    }
    gp := sg.g
    //解鎖channel
    unlockf()
    //接收groutian的param賦值
    gp.param = unsafe.Pointer(sg)
    if sg.releasetime != 0 {
        sg.releasetime = cputicks()
    }
    //喚醒groutian
    goready(gp, skip+1)
}

以上就是select的核心程式碼解析,可以對著註釋和上面的圖一起看,如果有一些channel的知識不是很明白,可以先看下channel詳解,相信你一定會有所收穫

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章