golang select底层原理

前言

select 是操作系统中的系统调用,我们经常会使用 selectpollepoll 等函数构建 I/O 多路复用模型提升程序的性能。Go 语言的 select 与操作系统中的 select 比较相似,但也有不同点,它只支持channel收发的多路复用。

这里已go1.19版本为例,编译器在中间代码生成期间会根据 selectcase 的不同对控制语句进行优化,这一过程都发生在 cmd/compile/internal/select/walk.go 的walkSelectCases()函数中,我们在这里会分四种情况介绍处理的过程和结果。

不存在任何case

select {

}
// cmd/compile/internal/select/walk.go
func walkselectcases(cases *Nodes) []*Node {
	n := cases.Len()

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

它直接将类似 select {} 的语句转换成调用 runtime.block函数:

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

只有一个case

如果当前的 select 条件只包含一个 case,那么编译器会将 select 改写成 if 条件语句。下面对比了改写前后的代码:

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

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

一个为正常case,一个为defalt case

编译后改写为:

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

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

selectnbsend和selectnbrecv实际上是一个非阻塞式地读写channel,在channel底层原理有介绍

至少有两个case

分为两种情况:

  • 两个case都不是default
  • 至少有三个case,且里面包含一个default
package main

import "fmt"
import "time"

func main() {
	ch1 := make(chan int)
	ch2 := make(chan int)
	go func() {
		time.Sleep(5 * time.Second)
		ch2 <- 1
	}()

	select {
	case <-ch1:
		fmt.Println("chan1 recv")
	case <-ch2:
		fmt.Println("chan2 recv")
	}
	fmt.Println("done")
}

此时两个channel都处于阻塞状态,这时候就是体现go的select实现多路复用的时候了。

主线程在执行select时,调用栈顺序如下:

// src/reflect/value.go

func Select(cases []SelectCase) (chosen int, recv Value, recvOK bool) {
	// ...
	chosen, recvOK = rselect(runcases)
	// ...
	return chosen, recv, recvOK
}

func rselect([]runtimeSelect) (chosen int, recvOK bool)

// src/runtime/select.go

//go:linkname reflect_rselect reflect.rselect
func reflect_rselect(cases []runtimeSelect) (int, bool) {
	if len(cases) == 0 {
		block()
	}
	// ...
	chosen, recvOK := selectgo(&sel[0], &order[0], pc0, nsends, nrecvs, dflt == -1)
	// ...
	return chosen, recvOK
}

这里来分析下 selectgo 的具体实现;

1. 打乱case顺序

func selectgo(cas0 *scase, order0 *uint16, pc0 *uintptr, nsends, nrecvs int, block bool) (int, bool) {
	...
	// 生成随机顺序
	norder := 0
	for i := range scases {
		cas := &scases[i]

		// 忽略轮询和锁定命令中没有通道的情况
		if cas.c == nil {
			cas.elem = nil // allow GC
			continue
		}

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

	// 根据 channel 地址进行排序,决定获取锁的顺序
	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]
	}
	...

	// 锁定所有的channel
	sellock(scases, lockorder)
	...
}

打乱顺序后,可以实现随机的执行。

2. 找出已经 ready 的 case

func selectgo(cas0 *scase, order0 *uint16, pc0 *uintptr, nsends, nrecvs int, block bool) (int, bool) {
	...
	var (
		gp     *g
		sg     *sudog
		c      *hchan
		k      *scase
		sglist *sudog
		sgnext *sudog
		qp     unsafe.Pointer
		nextp  **sudog
	)

	// pass 1 - 遍历所有 scase,确定已经准备好的 scase
	var casi int
	var cas *scase
	var caseSuccess bool
	var caseReleaseTime int64 = -1
	var recvOK bool
	// 因为上面已经将scases随机写入到pollorder中
	// 所以这里的遍历相比于原 cas0的顺序,就是随机的
	for _, casei := range pollorder {
		casi = int(casei)
		cas = &scases[casi]
		c = cas.c
		// 接收数据
		if casi >= nsends {
			// 有 goroutine 等待发送数据
			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
			}
			// 接收等待队列有 goroutine
			sg = c.recvq.dequeue()
			if sg != nil {
				goto send
			}
			// 缓冲区有空位置
			if c.qcount < c.dataqsiz {
				goto bufsend
			}
		}
	}

	// 如果不阻塞,意味着有 default,准备退出select
	if !block {
		selunlock(scases, lockorder)
		casi = -1
		goto retc
	}

	...

bufrecv:
	// 可以从 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)
	}
	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:
	// 可以发送到 buffer
	if raceenabled {
		racenotify(c, c.sendx, nil)
		raceReadObjectPC(c.elemtype, cas.elem, casePC(casi), chansendpc)
	}
	if msanenabled {
		msanread(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:
	// 可以从一个休眠的发送方 (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, casePC(casi), 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 caseReleaseTime > 0 {
		blockevent(caseReleaseTime-t0, 1)
	}
	return casi, recvOK

sclose:
	// 向已关闭的 channel 进行发送
	selunlock(scases, lockorder)
	panic(plainError("send on closed channel"))
}

遍历每一个scase,如果有一个已经ready了,就返回,结束select,以读取为例:

  • 1、如果有发送的 goroutine 在等待数据的接收,那么直接从这个 goroutine 中读出数据,结束 select;
  • 2、如果 channel 的缓冲区有数据,在缓冲去读出数据, 结束 select;
  • 3、如果 channel 关闭了,读出零值,结束 select。

可以看出select中的接收流程和channel的接收流程有点区别,channel的接收流程里是直接调用src/runtime/chan.go里的chanrecv()方法,该方法里有阻塞的情况,而select的接收流程里先是处理了所有非阻塞的情况,只要有一个scase是非阻塞的则返回,否则最后会gopark住当前协程

3. case 都没 ready,且没有 default

func selectgo(cas0 *scase, order0 *uint16, pc0 *uintptr, nsends, nrecvs int, block bool) (int, bool) {
	...
	// pass 2 - 所有 channel 入队,等待处理
	gp = getg()
	if gp.waiting != nil {
		throw("gp.waiting != nil")
	}
	nextp = &gp.waiting
	for _, casei := range lockorder {
		casi = int(casei)
		// 获取一个 scase
		cas = &scases[casi]
		// 监听的 channel
		c = cas.c
		// 构建sudog,设置这一次阻塞发送的相关信息
		sg := acquireSudog()
		sg.g = gp
		sg.isSelect = true // 标注这个sodug是由select产生的
		sg.elem = cas.elem
		sg.releasetime = 0
		if t0 != 0 {
			sg.releasetime = -1
		}
		sg.c = c
		// 按锁定顺序构造等待列表。
		*nextp = sg
		nextp = &sg.waitlink

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

	// goroutine 陷入睡眠,等待某一个 channel 唤醒 goroutine
	gp.param = nil

	atomic.Store8(&gp.parkingOnChan, 1)
    // 阻塞当前协程(gp),直到被其他goroutine唤醒
	gopark(selparkcommit, nil, waitReasonSelect, traceEvGoBlockSelect, 1)
    
	...
}
  1. 对每个scase构造一个对应的sudog,并把这些sudog分别分放到对应的channel等待队列中;
  2. gopark阻塞该协程,直到有任意一个操作channel的协程来唤醒这个协程。

4. 唤醒后返回 channel 对应的 case

func selectgo(cas0 *scase, order0 *uint16, ncase int)(int, bool) {
    // ...

    gopark(selparkcommit, nil,waitReasonSelect, traceEvGoBlockSelect, 1)

    // 对所有的 channel 加锁
    sellock(scases, lockorder)
    // 之前已经有很多sudog被放入chan的等待队列里,现在只有一个会被唤醒,那哪个是被唤醒的呢?
	// 关键逻辑:可以通过gp.param来获取已解除阻塞的那个sudog
    sg = (*sudog)(pg.param)
    gp.param = nil

    // waiting 链表按照 lockorder 顺序存放着 sudog
    sglist = gp.waiting

    casi = -1
    cas = nil // cas 便是唤醒 goroutine 的 case
    for _, casei := range lockorder {
        k = &scases[casei]
        if k.kind == caseNil {
            continue
        }
        // 如果相等说明,goroutine 是被当前 case 的 channel 收发操作唤醒的
        // 如果是关闭操作,那么 sg 为 nil, 不会对 cas 赋值
        if sg == sglist {
            casi = int(casei)
            cas = k
        } else {
            // goroutine 已经被唤醒,将 sudog 从相应的收发队列中移除
            c = k.c

            // func (q *waitq) dequeueSudoG(sgp *sudog)
            // dequeueSudoG 会通过 sudog.prev 和 sudog.next 将 sudog 从等待队列中移除
            if k.kind == caseSend {
                c.sendq.dequeueSudoG(sglist)
            } else {
                c.recvq.dequeueSudoG(sglist)
            }
        }

        // 释放 sudog,然后准备处理下一个 sudog
        sgnext = sglist.waitlink
        sglist.waitlink = nil
        releaseSudog(sglist)
        sglist = sgnext
    }
    ...
}

唤醒后的操作流程:

  1. 对所有的channel加锁;
  2. 获取当前的sudog,sudog里包含channel信息;
  3. 遍历每个scase,如果goroutine 是被当前 case 的 channel 收发操作唤醒的,则不做处理;
  4. 否则从这个channel中移除这个sudog;
  5. 对所有的channel解锁。

因为已经找到了一个可执行的 case,剩下的 case 中没有被用到的 sudog 就会被忽略并且释放掉。为了不影响 Channel 的正常使用,我们还是需要将这些废弃的 sudog 从 Channel 中出队。

posted @ 2023-03-29 15:01  独揽风月  阅读(280)  评论(0编辑  收藏  举报