golang-select

select的作用

golang 中的 select 就是用來監聽和 channel 有關的 IO 操作,當 IO 操作發生時,觸發相應的動作。select 只能應用於 channel 的操作,既可以用於 channel 的數據接收,也可以用於 channel 的數據發送。如果 select 的多個分支都滿足條件,則會隨機的選取其中一個滿足條件的分支執行。

基本用法

select {
    case <- chan1:
        // 如果 chan1 成功讀到數據,則進行該 case 處理語句
    case chan2 <- 1:
        // 如果成功向 chan2 寫入數據,則進行該 case 處理語句
    default:
        // 如果上面都沒有成功,則進入default處理流程
}

select原理

select結構

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

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

runtime\select.go

type scase struct {
    c           *hchan         // 當前case語句所操作的channel指針
    elem        unsafe.Pointer // data element數據元素
    kind        uint16 //表示該case的類型
    pc          uintptr // race pc (for race detector / msan)
    releasetime int64
}
//scase.kind values.
const (
    caseNil = iota
    caseRecv
    caseSend
    caseDefault
)

scase.kind表示該case的類型,分別爲:

  • caseRecv:case語句中嘗試讀取scase.c中的數據,case <-Chan;
  • caseSend:case語句中嘗試向scase.c中寫入數據,case Chan <- Send;
  • caseDefault:default

執行select

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

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

// cas0爲scase數組的首地址,selectgo()就是從這些scase中找出一個返回
// order0指向一個[2 * ncases] uint16類型的數組
// ncases表示scase數組的長度

//返回值:
//int :選中case的編號
//bool:是否成功從channle中讀取了數據
func selectgo(cas0 *scase, order0 *uint16, ncases int) (int, bool) {
    if debugSelect {
        print("select: cas0=", cas0, "\n")
    }

    cas1 := (*[1 << 16]scase)(unsafe.Pointer(cas0))
    order1 := (*[1 << 17]uint16)(unsafe.Pointer(order0))
    
    //將cas1從第一個元素開始切片,長度爲ncases,容量爲ncases
    //a[x:y:z] 切片長度: y-x 切片容量:z-x
    scases := cas1[:ncases:ncases]
    //所有case輪詢順序,佔了前面ncase
    pollorder := order1[:ncases:ncases]
    //所有case語句中channel序列,佔了後面ncase
    lockorder := order1[ncases:][:ncases:ncases]

    // 將涉及零個通道的發送/接收案例替換爲caseNil,因此以下邏輯可以假定爲非nil通道。
    for i := range scases {
        cas := &scases[i]
        if cas.c == nil && cas.kind != caseDefault {
            *cas = scase{}
        }
    }

    var t0 int64
    if blockprofilerate > 0 {
        t0 = cputicks()
        for i := 0; i < ncases; i++ {
            scases[i].releasetime = -1
        }
    }

    // 生成排列順序,pollorder重新排序
    for i := 1; i < ncases; i++ {
        j := fastrandn(uint32(i + 1))
        pollorder[i] = pollorder[j]
        pollorder[j] = uint16(i)
    }

    //通過Hchan地址進行排序以獲得鎖定順序
    //採用簡單的堆排序,以確保n log n個時間和恆定的堆棧佔用量
    for i := 0; i < ncases; i++ {
        j := i
        // 替換相同channel 的case,以達到去重防止對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
    }

    if debugSelect {
        for i := 0; i+1 < ncases; i++ {
            if scases[lockorder[i]].c.sortkey() > scases[lockorder[i+1]].c.sortkey() {
                print("i=", i, " x=", lockorder[i], " y=", lockorder[i+1], "\n")
                throw("select: broken sort")
            }
        }
    }

    //鎖住所有的channel
    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
    // 按照隨機順序檢測scase中的channel是否ready
    var dfli int
    var dfl *scase
    var casi int
    var cas *scase
    var recvOK bool
    //開始遍歷case數組了
    for i := 0; i < ncases; i++ {
        casi = int(pollorder[i])
        cas = &scases[casi]
        c = cas.c

        switch cas.kind {
        case caseNil:
            continue
        // 接收chan
        case caseRecv:
            sg = c.sendq.dequeue()
            // 當chan的等待寫隊列不爲空,需要等待
            if sg != nil {
                goto recv
            }
            //當chan的緩存隊列存在元素時,不需要等待
            if c.qcount > 0 {
                goto bufrecv
            }
            // 當chan關閉時
            if c.closed != 0 {
                goto rclose
            }
        //發送chan
        case caseSend:
            if raceenabled {
                racereadpc(c.raceaddr(), cas.pc, chansendpc)
            }
            // 當chan關閉時
            if c.closed != 0 {
                goto sclose
            }
            //當chan的等待讀消息的隊列不爲空,需要等待
            sg = c.recvq.dequeue()
            if sg != nil {
                goto send
            }
            // chan的緩存隊列的元素少於緩存容量時,還有位置,不需要等待
            if c.qcount < c.dataqsiz {
                goto bufsend
            }

        case caseDefault:
            dfli = casi
            dfl = cas
        }
    }

    if dfl != nil {
        selunlock(scases, lockorder)
        casi = dfli
        cas = dfl
        goto retc
    }

    // pass 2 - enqueue on all chans
    //所有case都未ready,且沒有default語句
    //將當前協程加入到所有channel的等待隊列
    gp = getg()
    if gp.waiting != nil {
        throw("gp.waiting != nil")
    }
    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
        // 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
        // Construct waiting list in lock order.
        *nextp = sg
        nextp = &sg.waitlink

        switch cas.kind {
        case caseRecv:
            // 加入等待接收隊列
            c.recvq.enqueue(sg)

        case caseSend:
            // 加入等待發送隊列
            c.sendq.enqueue(sg)
        }
    }

    // wait for someone to wake us up
    //當將協程轉入阻塞,等待被喚醒
    gp.param = nil
    gopark(selparkcommit, nil, waitReasonSelect, traceEvGoBlockSelect, 1)

    sellock(scases, lockorder)

    gp.selectDone = 0
    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.
    //喚醒後返回channel對應的case index
    casi = -1
    cas = nil
    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

    for _, casei := range lockorder {
        k = &scases[casei]
        if k.kind == caseNil {
            continue
        }
        if sglist.releasetime > 0 {
            k.releasetime = sglist.releasetime
        }
        if sg == sglist {
            // sg has already been dequeued by the G that woke us up.
            casi = int(casei)
            cas = k
        } else {
            c = k.c
            if k.kind == caseSend {
                c.sendq.dequeueSudoG(sglist)
            } else {
                c.recvq.dequeueSudoG(sglist)
            }
        }
        sgnext = sglist.waitlink
        sglist.waitlink = nil
        //釋放所有的鎖
        releaseSudog(sglist)
        sglist = sgnext
    }

    //沒找到case,重新循環
    if cas == nil {
        goto loop
    }

    c = cas.c

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

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

    if raceenabled {
        if cas.kind == caseRecv && cas.elem != nil {
            raceWriteObjectPC(c.elemtype, cas.elem, cas.pc, chanrecvpc)
        } else if cas.kind == caseSend {
            raceReadObjectPC(c.elemtype, cas.elem, cas.pc, chansendpc)
        }
    }
    if msanenabled {
        if cas.kind == caseRecv && cas.elem != nil {
            msanwrite(cas.elem, c.elemtype.size)
        } else if cas.kind == caseSend {
            msanread(cas.elem, c.elemtype.size)
        }
    }

    selunlock(scases, lockorder)
    goto retc

bufrecv:
    // 可以從緩衝區接收
    if raceenabled {
        if cas.elem != nil {
            raceWriteObjectPC(c.elemtype, cas.elem, cas.pc, chanrecvpc)
        }
        raceacquire(chanbuf(c, c.recvx))
        racerelease(chanbuf(c, c.recvx))
    }
    if msanenabled && cas.elem != nil {
        msanwrite(cas.elem, c.elemtype.size)
    }
    recvOK = true
    qp = chanbuf(c, c.recvx)
    if cas.elem != nil {
        // 將chan緩存中的數據拷貝到 case.elem。  eg: a := <-ch, a就是case.elem
        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:
    //可以發送到緩衝區
    if raceenabled {
        raceacquire(chanbuf(c, c.sendx))
        racerelease(chanbuf(c, c.sendx))
        raceReadObjectPC(c.elemtype, cas.elem, cas.pc, chansendpc)
    }
    if msanenabled {
        msanread(cas.elem, c.elemtype.size)
    }
    // 將cas.elem拷貝到chan的緩存中。eg: ch <- a, a 就是 cas.elem
    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

recv:
    //可以從休眠的發件人(sg)接收
    recv(c, sg, cas.elem, func() { selunlock(scases, lockorder) }, 2)
    if debugSelect {
        print("syncrecv: cas0=", cas0, " c=", c, "\n")
    }
    recvOK = true
    goto retc

rclose:
    //在封閉channel的末尾讀取
    selunlock(scases, lockorder)
    recvOK = false
    if cas.elem != nil {
        typedmemclr(c.elemtype, cas.elem)
    }
    if raceenabled {
        raceacquire(c.raceaddr())
    }
    goto retc

send:
    //可以發送到休眠的接收器(sg)
    if raceenabled {
        raceReadObjectPC(c.elemtype, cas.elem, cas.pc, chansendpc)
    }
    if msanenabled {
        msanread(cas.elem, c.elemtype.size)
    }
    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)
    }
    return casi, recvOK

sclose:
    //在關閉的channel上發送
    selunlock(scases, lockorder)
    panic(plainError("send on closed channel"))
}

1.初始化pollorder切片和lockorder切片,其中pollorder是存放所有case的,lockorder是存放所有channel
2.對pollorder進行重排序,打亂所有case的順序
3.鎖定scase語句中所有的channel
4.按照隨機順序檢測pollorder中case對應的channel是否ready:
  4.1 如果case可讀,則讀取channel中數據,解鎖所有的channel,然後返回(case index, true)
  4.2 如果case可寫,則將數據寫入channel,解鎖所有的channel,然後返回(case index, false)
  4.3 所有case都未ready,則解鎖所有的channel,然後返回(default index, false)
5.所有case都未ready,且沒有default語句
  5.1 將當前協程加入到所有channel的等待隊列
  5.2 當將協程轉入阻塞,等待被喚醒
6.喚醒後返回channel對應的case index
  6.1 如果是讀操作,解鎖所有的channel,然後返回(case index, true)
  6.2 如果是寫操作,解鎖所有的channel,然後返回(case index, false)總結

select語句中除default外,各case執行順序是隨機的
select語句中如果沒有default語句,則會阻塞等待任意一個case
select語句中讀操作要判斷是否成功讀取,關閉的channel也可以讀取
select語句中除default外,每個case只能操作一個channel,要麼讀要麼寫

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章