Go进阶之select

17 阅读8分钟

select是Go在语言层面提供的多路I/O复用机制.用于检测多个管道是否就绪(可读或

可写).特性与管道息息相关.

1.select特性:

1.1管道读写:

select只能作用于管道.包括数据的读取和写入.

func SelectChan(c chan string) {
    var recv string
    send := "hello"
    select {
    case recv = <-c:
       fmt.Println("recv:", recv)
    case c <- send:
       fmt.Println("send:", send)
    }
}

上面代码中有两个case语句.分别对应管道的写入和读取操作.至于执行哪个case操

作,取决于函数传入的管道.

情况1:管道没有缓冲区.

func main() {
    no := make(chan string)
    SelectChan(no)
}

此时管道既不能读也不能写.两个case语句都不执行.

情况2:管道有缓冲区.

func main() {
    no := make(chan string, 1)
    SelectChan(no)
}

此时管道可以写入.写操作对应的case语句得到执行.且执行结束后函数退出.

情况3:管道有缓冲区且缓冲区已放满数据.

func main() {
    no := make(chan string, 1)
    no <- "hello"
    SelectChan(no)
}

此时管道可以读取.读操作对应的case语句得到执行.且执行结束后函数退出.

情况4:管道有缓冲区,缓冲区有部分数据且还可以存入数据.

func main() {
    no := make(chan string, 2)
    no <- "hello"
    SelectChan(no)
}

此时管道既可以读取也可以写入.select随机挑选一个case语句执行.任意一个case语

句执行结束后函数退出.

总结:

select的每个case语句只能操作一个管道.要么写入数据.要么读取数据.鉴于管道的

特性.如果管道中没有数据读操作就会阻塞.如果管道没有空余的缓冲区写入.则会写阻

塞.当select的全部case语句的管道均阻塞,在没有default语句的前提下就会进入阻

塞.直到任意一个管道解除阻塞.如果多个case语句都没有阻塞将随机挑选一个执行结

束.

1.2返回值:

select为Go语言的预留关键字并非函数.其可以在case语句中声明变量并为变量赋

值.看上去就像一个函数.

case语句读取管道的时候.最多可以给两个变量赋值.

func SelectChan(c chan string) {
    send := "hello"
    select {
    case recv,ok := <-c:
       if ok {
          fmt.Println("recv:", recv)
       }
    case c <- send:
       fmt.Println("send:", send)
    }
}

case语句中管道的操作有两个返回条件.一个是成功独到的数据.第二个是管道已没有

数据且已关闭.

1.3default:

select语句中的default不能处理管道的读写操作.当select中所有语句都阻塞

时.default语句才能执行.

func SelectChan(c chan string) {
    send := "hello"
    select {
    case recv, ok := <-c:
       if ok {
          fmt.Println("recv:", recv)
       }
    case c <- send:
       fmt.Println("send:", send)
    default:
       fmt.Println("默认执行")
    }
}

default实际上是特殊的case.它能出现在select中的任意位置.但每个select语句中

只能出现一次.

2.实现原理:

2.1数据结构:

源码位于runtime/select.go中.

type scase struct {
    c    *hchan         // chan
    elem unsafe.Pointer // data element
}

c:   操作的管道.由于每个scase只能存放一个管道.这里直接决定了每个case语句只

能处理一个管道.编译器在处理case语句的时候.如果case语句中没有管道操作(不能

处理成sacse对象)则会给出编译错误.

elem:   数据存放的地址.

在类型为caseRecv的case中.elem表示从管道中读出的数据的存放地址.

在类型为caseSend的case中.elem表示写入管道的数据的存放地址.

2.2实现逻辑:

Go在运行的时候提供了selectgo()方法处理select语句.

func selectgo(cas0 *scase, order0 *uint16, pc0 *uintptr, nsends, nrecvs int, block bool) (int, bool) 

selectgo函数会从一组case语句中挑选一个case.并返回命中的case的下标.对于类

型为caseRecv的case.还会返回是否从管道中读取到数据.

2.2.1参数:

编译器会将select语句中的case语句存储在一个数组中.selectgo的第一个参数

case0就是这个参数的地址.参数ncases表示case的个数(包括default).即case0数

组的长度.

selectgo的第二个参数order0为一个整形数组的地址.长度为case个数的二

倍.order0数组是case执行随机性的关键.

2.2.2返回值:

当所有的case都不可能就绪时.selectgo()会陷入永久阻塞.此时函数不会返回.一旦

select()返回.就说明某个case语句就绪了.第一个返回值代表case的编号.这个编号

与代码出现的顺序一致.

第二个返回值代表是否从管道读取了数据.该值只针对类型为caseRecv的case有意

义.第二个返回值为true时.仅代表从管道读取了数据.对于已关闭通道也是一样的.

selectgo()源码:

func selectgo(cas0 *scase, order0 *uint16, pc0 *uintptr, nsends, nrecvs int, block bool) (int, bool) {
    gp := getg()
    if debugSelect {
       print("select: cas0=", cas0, "\n")
    }

    // NOTE: In order to maintain a lean stack size, the number of scases
    // is capped at 65536.
    cas1 := (*[1 << 16]scase)(unsafe.Pointer(cas0))
    order1 := (*[1 << 17]uint16)(unsafe.Pointer(order0))

    ncases := nsends + nrecvs
    //case数组切片
    scases := cas1[:ncases:ncases]
    //切取order0前半段.用于保存随机顺序.
    pollorder := order1[:ncases:ncases]
    lockorder := order1[ncases:][:ncases:ncases]
    // NOTE: pollorder/lockorder's underlying array was not zero-initialized by compiler.

    // Even when raceenabled is true, there might be select
    // statements in packages compiled without -race (e.g.,
    // ensureSigM in runtime/signal_unix.go).
    var pcs []uintptr
    if raceenabled && pc0 != nil {
       pc1 := (*[1 << 16]uintptr)(unsafe.Pointer(pc0))
       pcs = pc1[:ncases:ncases]
    }
    casePC := func(casi int) uintptr {
       if pcs == nil {
          return 0
       }
       return pcs[casi]
    }

    var t0 int64
    if blockprofilerate > 0 {
       t0 = cputicks()
    }

    // The compiler rewrites selects that statically have
    // only 0 or 1 cases plus default into simpler constructs.
    // The only way we can end up with such small sel.ncase
    // values here is for a larger select in which most channels
    // have been nilled out. The general code handles those
    // cases correctly, and they are rare enough not to bother
    // optimizing (and needing to test).

    // generate permuted order
    norder := 0
    allSynctest := true
    //过滤掉管道为nil的case.
    for i := range scases {
       cas := &scases[i]

       // Omit cases without channels from the poll and lock orders.
       if cas.c == nil {
          cas.elem = nil // allow GC
          continue
       }

       if cas.c.synctest {
          if getg().syncGroup == nil {
             panic(plainError("select on synctest channel from outside bubble"))
          }
       } else {
          allSynctest = false
       }

       if cas.c.timer != nil {
          cas.c.timer.maybeRunChan()
       }

       j := cheaprandn(uint32(norder + 1))
       pollorder[norder] = pollorder[j]
       pollorder[j] = uint16(i)
       norder++
    }
    pollorder = pollorder[:norder]
    lockorder = lockorder[:norder]

    waitReason := waitReasonSelect
    if gp.syncGroup != nil && allSynctest {
       // Every channel selected on is in a synctest bubble,
       // so this goroutine will count as idle while selecting.
       waitReason = waitReasonSynctestSelect
    }

    // sort the cases by Hchan address to get the locking order.
    // simple heap sort, to guarantee n log n time and constant stack footprint.
    for i := range lockorder {
       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 := len(lockorder) - 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 < len(lockorder); 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")
          }
       }
    }

    // lock all the channels involved in the select
    sellock(scases, lockorder)

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

    // pass 1 - look for something already waiting
    var casi int
    var cas *scase
    var caseSuccess bool
    var caseReleaseTime int64 = -1
    var recvOK bool
    for _, casei := range pollorder {
       casi = int(casei)
       cas = &scases[casi]
       c = cas.c

       if casi >= nsends {
          sg = c.sendq.dequeue()
          if sg != nil {
             goto recv
          }
          if c.qcount > 0 {
             goto bufrecv
          }
          if c.closed != 0 {
             goto rclose
          }
       } else {
          if raceenabled {
             racereadpc(c.raceaddr(), casePC(casi), chansendpc)
          }
          if c.closed != 0 {
             goto sclose
          }
          sg = c.recvq.dequeue()
          if sg != nil {
             goto send
          }
          if c.qcount < c.dataqsiz {
             goto bufsend
          }
       }
    }

    if !block {
       selunlock(scases, lockorder)
       casi = -1
       goto retc
    }

    // pass 2 - enqueue on all chans
    if gp.waiting != nil {
       throw("gp.waiting != nil")
    }
    nextp = &gp.waiting
    for _, casei := range lockorder {
       casi = int(casei)
       cas = &scases[casi]
       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

       if casi < nsends {
          c.sendq.enqueue(sg)
       } else {
          c.recvq.enqueue(sg)
       }

       if c.timer != nil {
          blockTimerChan(c)
       }
    }

    // wait for someone to wake us up
    gp.param = nil
    // Signal to anyone trying to shrink our stack that we're about
    // to park on a channel. The window between when this G's status
    // changes and when we set gp.activeStackChans is not safe for
    // stack shrinking.
    gp.parkingOnChan.Store(true)
    gopark(selparkcommit, nil, waitReason, traceBlockSelect, 1)
    gp.activeStackChans = false

    sellock(scases, lockorder)

    gp.selectDone.Store(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.
    casi = -1
    cas = nil
    caseSuccess = false
    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.c.timer != nil {
          unblockTimerChan(k.c)
       }
       if sg == sglist {
          // sg has already been dequeued by the G that woke us up.
          casi = int(casei)
          cas = k
          caseSuccess = sglist.success
          if sglist.releasetime > 0 {
             caseReleaseTime = sglist.releasetime
          }
       } else {
          c = k.c
          if int(casei) < nsends {
             c.sendq.dequeueSudoG(sglist)
          } else {
             c.recvq.dequeueSudoG(sglist)
          }
       }
       sgnext = sglist.waitlink
       sglist.waitlink = nil
       releaseSudog(sglist)
       sglist = sgnext
    }

    if cas == nil {
       throw("selectgo: bad wakeup")
    }

    c = cas.c

    if debugSelect {
       print("wait-return: cas0=", cas0, " c=", c, " cas=", cas, " send=", casi < nsends, "\n")
    }

    if casi < nsends {
       if !caseSuccess {
          goto sclose
       }
    } else {
       recvOK = caseSuccess
    }

    if raceenabled {
       if casi < nsends {
          raceReadObjectPC(c.elemtype, cas.elem, casePC(casi), chansendpc)
       } else if cas.elem != nil {
          raceWriteObjectPC(c.elemtype, cas.elem, casePC(casi), chanrecvpc)
       }
    }
    if msanenabled {
       if casi < nsends {
          msanread(cas.elem, c.elemtype.Size_)
       } else if cas.elem != nil {
          msanwrite(cas.elem, c.elemtype.Size_)
       }
    }
    if asanenabled {
       if casi < nsends {
          asanread(cas.elem, c.elemtype.Size_)
       } else if cas.elem != nil {
          asanwrite(cas.elem, c.elemtype.Size_)
       }
    }

    selunlock(scases, lockorder)
    goto retc

bufrecv:
    // can receive from buffer
    if raceenabled {
       if cas.elem != nil {
          raceWriteObjectPC(c.elemtype, cas.elem, casePC(casi), chanrecvpc)
       }
       racenotify(c, c.recvx, nil)
    }
    if msanenabled && cas.elem != nil {
       msanwrite(cas.elem, c.elemtype.Size_)
    }
    if asanenabled && cas.elem != nil {
       asanwrite(cas.elem, c.elemtype.Size_)
    }
    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:
    // can send to buffer
    if raceenabled {
       racenotify(c, c.sendx, nil)
       raceReadObjectPC(c.elemtype, cas.elem, casePC(casi), chansendpc)
    }
    if msanenabled {
       msanread(cas.elem, c.elemtype.Size_)
    }
    if asanenabled {
       asanread(cas.elem, c.elemtype.Size_)
    }
    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:
    // can receive from sleeping sender (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:
    // read at end of closed channel
    selunlock(scases, lockorder)
    recvOK = false
    if cas.elem != nil {
       typedmemclr(c.elemtype, cas.elem)
    }
    if raceenabled {
       raceacquire(c.raceaddr())
    }
    goto retc

send:
    // can send to a sleeping receiver (sg)
    if raceenabled {
       raceReadObjectPC(c.elemtype, cas.elem, casePC(casi), chansendpc)
    }
    if msanenabled {
       msanread(cas.elem, c.elemtype.Size_)
    }
    if asanenabled {
       asanread(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 caseReleaseTime > 0 {
       blockevent(caseReleaseTime-t0, 1)
    }
    return casi, recvOK

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

selectgo函数要点:

通过随机函数fastrandn()将原始的case顺序打乱.在遍历各个case时使用打乱后的

顺序就会表现出随机性.

循环遍历各个case时.如果发现某个case就绪(管道可读或可写).则直接跳出循环进行

管道操作并返回.

循环遍历各个case时.循环能正常结束.(没有跳转).说明所有case都没有就绪.如果有

default语句则命中default.

如果所有case都未命中且没有default.selectgo()将阻塞等待所有通道.任一管道就

绪后.都将开始新的循环.

***山水迢迢太匆忙.






如果大家喜欢我的分享的话.可以关注我的微信公众号

念何架构之路