for{}会让CPU跑满,因其无任何调度点,goroutine持续独占P并死循环执行空指令,导致调度器无法抢占、其他goroutine饿死;需用runtime.Gosched()、time.Sleep()或select{}显式让出或阻塞。

为什么 for {} 会让 CPU 跑满
Go 中写 for {} 确实是无限循环,但它不包含任何让出 CPU 的操作,编译器和运行时也不会自动插入休眠或调度点。结果就是 goroutine 在一个 OS 线程上死循环执行空指令,调度器无法抢占(因为没函数调用、没 channel 操作、没系统调用),该线程持续占用一个 CPU 核心。
用 runtime.Gosched() 主动让出时间片
这是最轻量的解法:在循环体内显式调用 runtime.Gosched(),告诉调度器“我先歇会儿,换别人跑”。它不会阻塞,只是把当前 goroutine 放回运行队列尾部,等待下次被调度。
适用场景:需要高频轮询但又不想吃满 CPU,比如简单状态检查、调试用的活锁规避。
package main
<p>import (
"runtime"
"time"
)</p><p>func main() {
go func() {
for {
// 做点事(比如检查某个标志)
// if flag.Load() { ... }</p><pre class="brush:php;toolbar:false;"> runtime.Gosched() // 关键:主动让出
}
}()
time.Sleep(time.Second) // 防止主 goroutine 退出
}
-
runtime.Gosched()开销极小,但不能替代 sleep;它不保证休眠时长,只保证“这次不继续霸占” - 如果循环体本身有 I/O、channel 操作或函数调用(哪怕只是
fmt.Print),通常已隐含调度点,不一定需要手动加 - 别在性能敏感路径里滥用——频繁调用它相当于把循环变成“短时分时抢占”,仍可能抬高调度器压力
用 time.Sleep() 控制实际执行频率
绝大多数真实场景下,你并不需要“每纳秒都检查一次”,而是希望“每隔若干毫秒/秒做一次”。这时 time.Sleep() 是更合理、更可控的选择。
常见错误:用 time.Sleep(0) 试图“让出但不休眠”——它等价于 runtime.Gosched(),但语义模糊,且 Go 1.22+ 已明确文档标注“不保证让出”,应避免。
- 设休眠时间要匹配业务需求:轮询网络连接健康?用
time.Second;监听文件变化?100 * time.Millisecond可能更合适 - 注意
time.Sleep()会阻塞当前 goroutine,但不会阻塞整个程序——这是 Go 并发模型的优势 - 如果休眠期间需要响应中断(比如收到 shutdown 信号),得配合
context.Context和time.AfterFunc或 select + timer
用 select + time.Ticker 实现可取消的定时循环
当循环需要支持优雅退出、与其他 channel 协作,或者需严格对齐时间间隔(如每 500ms 执行一次),time.Ticker 是标准做法。
ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop()
<p>for {
select {
case <-ticker.C:
// 执行周期性任务
case <-done: // done 是 context.Done() 或自定义 close chan
return
}
}
-
time.Ticker底层使用高效的定时器堆,比反复time.Sleep()更精准、资源更省 - 必须调用
ticker.Stop(),否则 ticker 会泄漏 goroutine 和定时器资源 - 不要用
time.Tick()(它是无缓冲的全局 ticker),它无法 Stop,也不适合长期运行
真正难的不是选哪个 API,而是想清楚:这个循环到底要响应多快?是否允许延迟?要不要支持中断?CPU 占用只是表象,背后是并发意图和资源契约。漏掉 Stop()、误用 time.Tick()、或在不该让出的地方狂打 Gosched(),比单纯 CPU 高更难排查。


















