Go面试:select是随机的还是顺序的?

  1. select语句不使用default分支时,处于阻塞状态直到其中一个channel的收/发操作准备就绪(或者channel关闭或者缓冲区有值),如果同时有多个channel的收/发操作准备就绪(或者channel关闭)则随机选择其中一个。
  2. select语句使用default分支时,处于非阻塞状态,从所有准备就绪(或者channel关闭或者缓冲区有值)的channel中随机选择其中一个,如果没有则执行default分支。

严格来讲,select的实现包括两种顺序:一是:轮询顺序(poll order),二是加锁/解锁顺序(lock order)。个人理解,这道题的本意应该是在问轮询顺序。

轮询顺序的实现: 首先将所有case分支进行随机排序,然后按照这个随机顺序来遍历case分支,选择第一个符合条件(就绪或关闭或缓冲区有值)的channel即返回不再遍历后面的case分支。

runtime/select.go对应源码节选:

调用fastrandn来随机排序pollorder

	// generate permuted order
	for i := 1; i < ncases; i++ {
		j := fastrandn(uint32(i + 1))
		pollorder[i] = pollorder[j]
		pollorder[j] = uint16(i)
	}


loop:
	// pass 1 - look for something already waiting
	var dfli int
	var dfl *scase
	var casi int
	var cas *scase
	var recvOK bool
	for i := 0; i < ncases; i++ {
		casi = int(pollorder[i])
		cas = &scases[casi]
		c = cas.c

		switch cas.kind {
		case caseNil:
			continue

		case caseRecv:
			sg = c.sendq.dequeue()
			if sg != nil {
				goto recv
			}
			if c.qcount > 0 {
				goto bufrecv
			}
			if c.closed != 0 {
				goto rclose
			}

		case caseSend:
			if raceenabled {
				racereadpc(c.raceaddr(), cas.pc, chansendpc)
			}
			if c.closed != 0 {
				goto sclose
			}
			sg = c.recvq.dequeue()
			if sg != nil {
				goto send
			}
			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
	}

不使用default(阻塞)验证如下: 

select_block.go

package main

import (
	"fmt"
	"time"
)

func main() {
	ch1 := make(chan int)
	ch2 := make(chan int)
	var cnt [2]int
	for i := 0; i < 1000; i++ {
		go func(c1 chan int) {
			c1 <- 0
		}(ch1)

		go func(c2 chan int) {
			c2 <- 1
		}(ch2)

		//等10毫秒,确保两个channel都已准备就绪
		time.Sleep(10 * time.Millisecond)

		var index int

		select {
		case index = <-ch1:
			cnt[index]++
		case index = <-ch2:
			cnt[index]++
		}
	}
	fmt.Printf("cnt=%v\n", cnt)
}

 连续执行多次,可以看到每次数字都不同,但比例接近于1 :1而且总和是1000,说明在同时接收到两个channel的返回时select等概率随机选择其中一个,而将另一个舍弃。

[root@dev tutorial]# go run select_block.go
cnt=[507 493]
[root@dev tutorial]# go run select_block.go
cnt=[496 504]
[root@dev tutorial]# go run select_block.go
cnt=[492 508]
[root@dev tutorial]# go run select_block.go
cnt=[493 507]
[root@dev tutorial]# go run select_block.go
cnt=[499 501]
[root@dev tutorial]# go run select_block.go
cnt=[505 495]

使用default分支(非阻塞)的验证:

select_nonblock.go

package main

import (
	"fmt"
	"time"
)

func main() {
	var cnt [2]int
	for i := 0; i < 1000; i++ {
		timer1 := time.NewTimer(5 * time.Millisecond)
		defer timer1.Stop()

		timer2 := time.NewTimer(5 * time.Millisecond)
		defer timer2.Stop()

		//等10毫秒,确保两个channel都已准备就绪
		time.Sleep(10 * time.Millisecond)

		select {
		case <-timer1.C:
			cnt[0]++
		case <-timer2.C:
			cnt[1]++
		default:
		}
	}

	fmt.Printf("cnt=%v\n", cnt)

}

输出:

[root@dev tutorial]# go run select_nonblock.go
cnt=[498 502]
[root@dev tutorial]# go run select_nonblock.go
cnt=[544 456]
[root@dev tutorial]# go run select_nonblock.go
cnt=[520 480]

相关文章:

《Go语言:为什么要使用上下文(context)而不是计时器(timer)加通道(channel)的方式来控制协程》

《Go语言:select的典型应用场景之IO超时控制的示例(阻塞方式)》

《Go语言:select的典型应用场景之IO超时控制的示例(采用default,非阻塞方式)》

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值