Go语言中select的default分支调用runtime.Gosched()是否有意义?
Go官方文档对runtime.Gosched()的说明如下:
Gosched会让出处理器,允许其他goroutine运行。它不会挂起当前goroutine,执行会自动恢复。
很多人看到这段说明会想当然认为,在轮询逻辑里加runtime.Gosched()能让更多goroutine拿到运行机会,但这个假设完全不成立,题目里的select写法不仅没有调度优势,反而会带来非常明显的性能损耗,是典型的Go并发反模式。
核心问题在哪
- 带
default分支的select本身是非阻塞的:只要msgch当前没有可读消息,代码会立刻进入default分支,不会产生任何阻塞等待。这时候调用runtime.Gosched(),确实会让当前goroutine暂时让出逻辑处理器(P)的使用权,把自己放回运行队列等待下次调度,但因为外层是没有任何阻塞的死循环,这个goroutine被重新调度到之后,会立刻再次进入循环检查channel状态——没消息就再次让出、重新排队,整个过程无限循环,平白产生巨量无意义的调度开销,直接占满1个CPU核心。这种高频的调度进出反而会挤占其他goroutine的调度资源,根本达不到“提升公平性”的效果。
你可以写个简单demo测试:用题目里的写法,哪怕channel永远不会有消息进来,程序的CPU使用率也会稳定跑满单核;而去掉default分支的写法,没消息的时候CPU使用率基本为0,性能差距非常明显。 - Go原生的channel阻塞机制本身就做了最优的调度适配:你完全不需要加default和Gosched,直接写最简单的循环读channel就行:
当channel没有可读消息时,当前goroutine会被调度器直接标记为等待状态,彻底让出CPU资源,调度器会把所有时间片分配给其他可运行的goroutine,直到for { msg := <-msgch fmt.Println(msg) }msgch有新消息到来时,才会把这个等待的goroutine唤醒重新加入运行队列。整个过程是事件驱动的,没有任何轮询开销,调度效率和公平性远高于手动调用Gosched的写法。 - 现代Go版本(1.14及以上)已经实现了基于信号的异步抢占:就算是长时间跑满CPU的计算密集型goroutine,最长连续运行10ms就会被调度器强制抢占,根本不需要开发者手动调用Gosched来保证其他goroutine能拿到运行权。只有在极个别纯计算死循环、且运行在Go1.14以前老版本的场景下,Gosched才偶尔能起到主动让权的作用,在非阻塞select轮询里加它完全是负优化。
补充说明:
runtime.Gosched()的设计初衷是给极少数没有任何抢占点的特殊计算场景提供主动让出的入口,从来不是用来搭配非阻塞select做忙轮询的。这种default+Gosched的写法本质是把Go高效的事件驱动调度退化成了低效的忙等,任何生产场景都不推荐使用。
内容的提问来源于stack exchange,提问作者Arnold Zahrneinder
相关产品推荐
相关产品推荐

