在Go语言并发编程中,Goroutine是轻量级线程,由runtime调度器管理。不少初学者写并发代码时会发现,当循环启动Goroutine的次数分别为奇数和偶数时,程序输出或退出行为似乎不一样,于是怀疑调度器对奇偶数循环次数有特殊处理。本文从调度机制和实际编码角度说明其中的真相。

调度器如何看待循环次数
Go调度器采用GMP模型,G代表Goroutine,M是系统线程,P是处理器上下文。调度器负责把G分配到P上执行,其决策依据的是就绪队列、系统调用阻塞、抢占时间片等,而不是外层for循环的计数值是奇数还是偶数。换句话说,循环次数本身不是调度算法的输入参数。
为什么奇偶会让人感觉有影响
常见错觉来自主函数提前退出。若启动的Goroutine数量为奇数,而开发者只用固定次数的接收来等结果,可能少等一个;偶数时刚好配对,程序看似正常。这其实是同步逻辑漏洞,不是调度偏好奇偶。
- 奇数次循环可能留下未回收的Goroutine,主协程结束致其被迫终止
- 偶数次循环若配合错误计数,也会丢失任务
- 真正决定可见行为的是WaitGroup或channel的使用是否正确
用同步原语消除不确定性
不要依赖循环奇偶去猜测执行顺序,应使用sync.WaitGroup保证全部完成。下面示例展示无论循环次数是七还是八,逻辑都安全。
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
// 循环次数可任意,奇偶不影响正确性
for i := 0; i < 7; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
fmt.Println("goroutine", id)
}(i)
}
wg.Wait()
fmt.Println("all done")
}
通过channel控制并发节奏
若需限制并发数或收集结果,channel比数循环奇偶更可靠。以下代码用缓冲channel做信号量,与次数奇偶无关。
package main
import (
"fmt"
)
func main() {
sem := make(chan struct{}, 3)
results := make(chan int, 10)
// 偶数次循环示例,改为奇数同样工作
for i := 0; i < 6; i++ {
sem <- struct{}{}
go func(v int) {
defer func() { <-sem }()
results <- v * v
}(i)
}
// 关闭需在另处,此处简化示意
for i := 0; i < 6; i++ {
fmt.Println(<-results)
}
}
小结
Goroutine调度不受循环次数奇偶性直接影响。所谓差异多源于同步缺失或主协程退出时机问题。写并发程序应显式同步,理解<code>WaitGroup</code>与<code>channel</code>的用法,避免用奇数偶数循环去试探调度行为。