golang的select语句是平时编码的常用特性,结合golang特有的channel机制,goroutine用起来爽到飞起有木有。
基本用法
这里先提一下简单用法,老手可跳过。
根据官方文档
其实很类似于switch语句,只不过case语句上有些差别——case只能引用通信操作。什么是“通信操作”呢?就可以理解为对chan的读取或写入操作啦!
其表达意义在于,当多个需要从多个chan中读取或写入时,会先轮询一遍所有的case,然后在所有处于就绪(可读/可写)的chan中随机挑选一个进行读取或写入操作,并执行其语句块。如果所有case都未就绪,则执行default语句,如未提供default语句,则当前协程被阻塞。
select可以用于监听异步调用,注意在异步调用时我们一般会在函数返回值上使用chan,用以通知调用方结果,有点类似于java里的Future。所以我们只需在case语句中对异步调用进行chan的读取操作就可以了,看下边的例子:
代码逻辑很简单,两个异步调用,任务耗时,完成之后将耗时写入chan。执行3次调用,分别指定耗时50ms,200ms,3000ms,然后select一下,打印最先完成的异步调用耗时。
温柔陷阱
公平性问题
在上边的代码中,第一个异步调用耗时50ms,第二个耗时200ms,第三个耗时3000ms,根据select的逻辑,肯定是第一个调用最先处于就绪状态,那么打印结果必然只可能是50。
是吗?运行一下试试看?
肯定是不对的,这段代码运行的结果会是200和50两种结果随机出现。
这就引出了select的公平性问题。
凭什么第二个调用能在完成时间落后的情况下被select选中?select不是能保证先就绪的case被先执行吗?难道是golang的bug?
相信仔细看过代码你就能发现,问题主要出在c的异步调用AsyncCall2上,由于这个异步调用本身的执行的时间为200ms,超过了前两个的任务执行时间。而根据我们之前提到的,select语句在判断chan就绪之前,是会把所有的case语句的判断语句执行一遍的,这就包括了在case语句中的函数调用,因此上边的代码执行逻辑为:AsyncCall(50)--->AsyncCall(200)--->AsyncCall3(3000),然后再等待三者返回的chan就绪,谁先就绪就执行谁;如果三者同时就绪,则随机挑选一个执行;没有就绪就阻塞直到有一个就绪。显然当AsyncCall3(3000)执行完成时,前两个异步任务已经完成,因此在判断chan就绪时就是随机选择前两个chan了,故而打印结果为50和200随机出现。
超时
一般在可能发生阻塞的select语句中我们会加上超时检测,如go源码测试代码片段:
这里使用time.After进行超时检测,当时间超过1s后如果chan仍未就绪,则执行time.After对应的case语句块。
照猫画虎,我们把这个超时检测机制加入到异步函数调用中,比如我们希望调用时间不能超过100ms,超过100ms就结束阻塞并报错:
执行结果为:
调用耗时200ms,超时设置并没有起到作用。
究其原因,还是AsyncCall2本身耗时所致。前边说过,select在执行case的判断语句时是按顺序执行的,因此AsyncCall2在time.After之前执行,所以当time.After开始计时时,已经过了200ms了。
结论
- 为了你我和他人的安全,请尽量在返回chan的函数中减少耗时操作;
- 如果是select 的case执行顺序敏感的情形,请尽量不要在case中调用函数;