
go 程序中若在 `main` 函数末尾使用空 `for{}` 死循环,会导致调度器无法切换到其他 goroutine;根本原因在于该循环不触发调度点,新启动的 goroutine 来不及被调度即被“饿死”。解决方法是插入调度让点(如 `time.sleep`)或改用阻塞式等待(如 `select{}`)。
在 Go 中,并发调度依赖于协作式调度机制:goroutine 只有在发生系统调用、通道操作、time.Sleep、runtime.Gosched() 或函数调用(可能引发栈增长/检查)等调度点(scheduling points) 时,才会主动让出 CPU,供运行时调度器选择其他就绪的 goroutine 执行。
你提供的代码存在一个典型误区:
func main() {
runtime.GOMAXPROCS(runtime.NumCPU() * 8) // ✅ 设置最大 OS 线程数(但非必需)
go func() {
for {
time.Sleep(1 * time.Second)
fmt.Println("From routine")
}
}()
for {} // ❌ 危险!无限空循环 —— 无调度点、不释放 CPU、不触发 GC 检查、完全阻塞当前 M/P
}尽管 GOMAXPROCS 控制了可并行执行的 OS 线程上限(影响 CPU 密集型任务的并行度),但它不能绕过调度器的基本规则。问题核心并非线程数量不足,而是 for{} 导致 main goroutine 永不交出控制权,调度器根本没有机会将后台 goroutine 分配到其他线程(甚至同一 P 上)执行——它甚至可能还没来得及被放入运行队列。
✅ 正确做法一:插入轻量级调度让点
在死循环前添加微小休眠,确保 goroutine 已启动且调度器有机会介入:
func main() {
runtime.GOMAXPROCS(runtime.NumCPU() * 8)
go func() {
for i := 0; i < 5; i++ {
time.Sleep(1 * time.Second)
fmt.Println("From goroutine:", i)
}
}()
time.Sleep(time.Millisecond) // ✅ 关键:让出当前时间片,触发调度
for {} // 此时后台 goroutine 极大概率已开始运行
}⚠️ 注意:time.Sleep(time.Millisecond) 并非“必须 1ms”,任何非零 Sleep(如 1ns)均可触发调度;但实践中建议 ≥1ms 以兼顾可靠性和精度。
✅ 更优做法二:使用 select{} 阻塞等待(推荐)
select{} 在无 case 可选时会永久阻塞,且完全不消耗 CPU,同时保持调度器活跃,是等待 goroutine 完成的标准惯用法:
func main() {
// GOMAXPROCS 可省略(默认已是 NumCPU)
go func() {
for i := 0; i < 5; i++ {
time.Sleep(1 * time.Second)
fmt.Println("From goroutine:", i)
}
}()
select {} // ✅ 完美阻塞:零 CPU 占用,允许所有 goroutine 公平调度
}? 补充说明:
- LockOSThread() 在此场景无效且有害:它将 main goroutine 绑定到某个 OS 线程,反而限制了调度灵活性,且空循环仍会独占该线程。
- GOMAXPROCS 对 I/O 密集型 goroutine(如含 time.Sleep、网络请求)影响有限,其主要价值在于提升 CPU 密集型任务的并行吞吐。
- 若需等待多个 goroutine 结束,应使用 sync.WaitGroup + select{} 或带超时的 time.After,而非轮询或空循环。
总结:Go 的并发模型建立在合作调度之上,编写无调度点的纯计算死循环(for{})是反模式。始终优先选用 select{}、time.Sleep、通道阻塞等内置调度友好的原语,才能真正释放 Go 调度器的强大能力。









