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,要么读要么写

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