如何让Golang调度器在继续执行前运行所有goroutine?runtime.Gosched()无效
问题背景
想要让Go调度器在主逻辑(start()函数中的select语句)执行前运行所有goroutine,但尝试使用runtime.Gosched()无法解决问题。核心矛盾在于:stopStream() goroutine可能执行过快,导致start()中的select晚于stopStream()的select执行。此时start()里的case <-chanStopStream:接收端尚未就绪,而stopStream()里的case retChan <- true:发送端已就绪,最终触发stopStream()的default分支,出现不符合预期的执行结果。
多次运行代码会出现两种结果:
- 正常执行输出:
2009/11/10 23:00:00 Start
2009/11/10 23:00:00 receive chan
2009/11/10 23:00:03 end
- 异常执行输出:
2009/11/10 23:00:00 Start
2009/11/10 23:00:00 default
2009/11/10 23:00:01 TIMER
2009/11/10 23:00:04 end
问题根源
- 无缓冲通道的特性:无缓冲通道的发送操作必须等待接收端就绪才能完成。当
stopStream()先执行到select时,start()还没进入自己的select,此时没有接收方,发送操作会直接触发default分支。 runtime.Gosched()的局限性:这个函数只是让当前goroutine让出CPU,但Go调度器不保证其他goroutine一定会优先完成执行,调度行为具有不确定性,因此无法依赖它来保证固定的执行顺序。
解决方案
方案1:使用带缓冲的通道
将无缓冲通道改为带1个缓冲的通道,这样发送操作可以直接把值存入缓冲区,无需等待接收端就绪。即使stopStream()先执行,发送也能成功,后续start()的select就能正常接收到值。
修改代码中通道创建的行:
chanStopStream := make(chan bool, 1) // 带1个缓冲的通道
方案2:用同步原语保证执行顺序
使用sync.WaitGroup让stopStream()等待start()准备好接收后再执行发送逻辑,确保接收端先就绪。
修改后的关键代码:
func start() { log.Println("Start") chanStopStream := make(chan bool) var streamWg sync.WaitGroup streamWg.Add(1) go stopStream(chanStopStream, &streamWg) streamWg.Done() // 通知stopStream可以继续执行 select { case <-chanStopStream: log.Println("receive chan") case <-time.After(time.Second): log.Println("TIMER") } time.Sleep(3 * time.Second) log.Println("end") wg.Done() } func stopStream(retChan chan bool, wg *sync.WaitGroup) { wg.Wait() // 等待start()准备好接收 // 原有的业务逻辑 select { case retChan <- true: default: log.Println("default") } }
方案3:用通道同步就绪状态
通过额外的通道来同步start()和stopStream()的状态,确保start()进入接收状态后,stopStream()再执行发送操作。
修改后的关键代码:
func start() { log.Println("Start") chanStopStream := make(chan bool) ready := make(chan struct{}) go stopStream(chanStopStream, ready) <-ready // 等待stopStream发出就绪信号,此时当前goroutine已进入接收等待状态 select { case <-chanStopStream: log.Println("receive chan") case <-time.After(time.Second): log.Println("TIMER") } time.Sleep(3 * time.Second) log.Println("end") wg.Done() } func stopStream(retChan chan bool, ready chan struct{}) { // 原有的业务逻辑 close(ready) // 通知start()已准备好,此时start()会进入接收状态 select { case retChan <- true: default: log.Println("default") } }
总结
Go的goroutine调度行为是不确定的,永远不要依赖调度顺序来保证逻辑正确性。对于通道操作,要么利用缓冲通道的特性解耦发送和接收的时机,要么使用同步原语明确控制执行顺序,从根源上避免这类调度引发的异常。
内容的提问来源于stack exchange,提问作者André Müller Pereira

