go的time.Sleep与time.Ticker
go time.Ticker与time.Sleep
核心实现机制
time.Sleep:
通过向 Go 运行时的定时器堆插入一次性定时器实现。
每次调用会触发定时器注册和销毁,产生以下开销:- 堆插入/删除操作(复杂度 O(log n))
- 上下文切换(Goroutine 休眠/唤醒)
time.Ticker:
创建周期性定时器,复用底层时间轮(Time Wheel)资源。
主要开销包括:- 初始化时的定时器注册
- 周期性的通道事件发送
- 事件接收时的调度
- 上下文切换(Goroutine 休眠/唤醒)
误差来源
-
- 唤醒:当goroutine因等待
ticker.C或Sleep到期被阻塞时,会进入_Gwaiting状态,并解绑当前的M(系统线程)。 - 定时器触发:时间到达后,调度器将goroutine标记为可运行(
_Grunnable),并将其加入目标P的本地运行队列。 - 队列满载时的处理:如果P的本地队列已满(默认容量为256),goroutine会被转移到全局队列。
- 唤醒:当goroutine因等待
-
时间误差来源:
- 全局队列调度延迟:全局队列的获取需要全局锁,高并发时竞争激烈,导致goroutine调度延迟。
- 工作窃取(Work Stealing)延迟:空闲的P会从全局队列或其他P的本地队列窃取任务,但这一过程非实时。
-
误差公式:
实际误差 = 理论间隔 + 全局队列调度延迟 + 上下文切换耗时其中:
- 全局队列调度延迟:通常为微秒级(10μs~1ms),但在极端高负载下可能达毫秒级
- 上下文切换耗时:约 0.1~0.3μs(现代CPU)
异同点
一、调度行为的相似性
| 行为特征 | time.Sleep |
<-ticker.C |
|---|---|---|
| Goroutine 状态 | 进入_Gwaiting状态 |
进入_Gwaiting状态 |
| M(线程)绑定 | 解除当前 G 与 M 的绑定 | 解除当前 G 与 M 的绑定 |
| 调度器介入 | 触发 Go 调度器进行上下文切换 | 同左 |
| CPU 占用 | 零占用(完全让出 CPU) | 同左 |
二、底层实现的差异性
| 实现特性 | time.Sleep |
time.Ticker |
|---|---|---|
| 计时器类型 | 一次性计时器 | 周期性计时器 |
| 资源开销 | 每次调用创建/销毁计时器对象 | 单次创建长期复用 |
| 时间精度控制 | 依赖全局计时器堆 | 基于分层时间轮算法 |
| 内存占用 | 临时分配(每次调用 32B) | 长期持有(每个 Ticker 48B) |
| 唤醒后行为 | 直接继续执行后续代码 | 需处理通道事件可能堆积 |
性能对比
package main
import (
"testing"
"time"
)
func BenchmarkSleep(b *testing.B) {
for i := 0; i < b.N; i++ {
time.Sleep(1 * time.Millisecond)
}
}
func BenchmarkTicker(b *testing.B) {
ticker := time.NewTicker(1 * time.Millisecond)
defer ticker.Stop()
for i := 0; i < b.N; i++ {
<-ticker.C
}
}
goos: darwin
goarch: arm64
pkg: test
cpu: Apple M2
BenchmarkSleep-8 1053 1229858 ns/op 0 B/op 0 allocs/op
BenchmarkTicker-8 1197 1000948 ns/op 0 B/op 0 allocs/op
## 分配内存为0,sleep本身不会分配内存,只会挂起goroutine;ticker在循环声明,复用计时器,不分配内存
time.Sleep:每次调用触发完整的 Goroutine 休眠/唤醒,产生O(log n)堆操作。time.Ticker:通过共享定时器和事件批处理,减少调度器介入。
故多次使用时time.Ticker性能优于sleep

浙公网安备 33010602011771号