跳转到主内容
极星编程网:以代码为星,赴技术山海!

Go 中 Goroutine 不执行的根本原因:for{} 阻塞调度器

当主 goroutine 陷入无限空循环 for{} 时,Go 运行时无法调度其他 goroutine 执行——即使它们已成功启动,也会永远“休眠”在就绪队列中,导致看似“未启动”的假象。 当主 goroutine 陷入无限空循环 `for{}` 时,go 运行时无法调度其他 goroutine 执行——即使它们已成功启动,也会永远“休眠”在就绪队列中,导致看似“未启动”的假象。 在 Go 并发模型中,goroutine 的执行依赖于 Go 调度器(Goroutine Scheduler)的主动调度。而调度器本身需要运行机会:它通常在以下时机介入——函数调用、channel 操作、系统调用、垃圾回收暂停点,或显式让出控制权(如 runtime.Gosched())。但一个纯 CPU 密集型的 for{} 循环 不触发任何上述事件 ,它会持续独占当前 OS 线程(M),使调度器彻底“失声”。 这正是你代码中问题的核心:
func main() { file_names := get_file_names() for _, file_name := range file_names { log.Println("Now launching goroutine for file", file_name) go mygoroutine(file_name) // ✅ 启动成功!但后续无调度机会 } log.Println("Finished launching.") for {} // ❌ 致命陷阱:永不释放 CPU,调度器无法运行 log.Println("Now exiting") // 永远不会执行 }
尽管 go mygoroutine(...) 语句被正确解析、goroutine 已被创建并加入就绪队列(/sched/goroutines/runnable 指标将显示其存在),但由于主 goroutine 卡死在 for{},Go 运行时无法切换到其他 goroutine,因此 "In goroutine for file..." 日志永远不会打印。 ? 验证提示 :可通过 Go 1.26 新增的运行时指标实时观测这一状态:
import "runtime/metrics" func printGoroutineStats() { m := metrics.Read(metrics.All()) for _, v := range m { if strings.HasPrefix(v.Name, "/sched/goroutines/") { fmt.Printf("%s: %d\n", v.Name, v.Value.Uint64()) } } }
你会看到 /sched/goroutines/runnable > 0,而 /sched/goroutines/running 长期为 1(仅 main),证实 goroutine 就绪却无法运行。 ✅ 正确做法:用 select{} 替代 for{} select{} 是 Go 中实现“永久阻塞且让出调度权”的惯用模式。它不消耗 CPU,同时允许调度器持续工作:
func main() { file_names := get_file_names() for _, file_name := range file_names { log.Println("Now launching goroutine for file", file_name) go mygoroutine(file_name) } log.Println("Finished launching.") select {} // ✅ 安全阻塞:调度器可自由调度其他 goroutine }
⚠️ 注意:select{} 本身不会导致程序退出;它只是让 main goroutine 永久等待(无 case 可选),但此时其他 goroutine 可正常执行并完成。不过,若所有 goroutine 结束后 main 仍卡在 select{},程序将因“all goroutines are asleep — deadlock!”崩溃——这反而是 Go 的健康保护机制,提示你 缺少同步逻辑 。 ✅ 生产级推荐:使用 sync.WaitGroup 显式等待 真正健壮的方案是明确协调生命周期,而非依赖阻塞原语:
package main import ( "log" "sync" "io/ioutil" "strings" ) const BASE_FILE_NAME = "abc_" func mygoroutine(file_name string, wg *sync.WaitGroup) { defer wg.Done() log.Println("In goroutine for file", file_name) } func get_file_names() []string { file_names := make([]string, 0) files, _ := ioutil.ReadDir("./") for _, file := range files { if strings.HasPrefix(file.Name(), BASE_FILE_NAME) { file_names = append(file_names, file.Name()) } } return file_names } func main() { var wg sync.WaitGroup file_names := get_file_names() for _, file_name := range file_names { log.Println("Now launching goroutine for file", file_name) wg.Add(1) go mygoroutine(file_name, &wg) } log.Println("Finished launching.") wg.Wait() // ✅ 等待所有 worker 完成 log.Println("All goroutines finished. Now exiting.") }
? 关键总结 for{} ≠ “等待”,而是 调度器拒绝服务 ;它是 Go 并发编程中最隐蔽的反模式之一。 select{} 是语义正确的永久阻塞方式,适用于守护型长期运行 goroutine(如监听 channel),但需配合退出机制。 sync.WaitGroup 或带缓冲 channel 是处理“启动 N 个任务并等待全部完成”的标准、可读、可维护方案。 始终牢记 Go 的并发哲学: “不要通过共享内存来通信;而应通过通信来共享内存。” —— 避免竞态的同时,也请信任调度器,别亲手把它锁死。

相关文章