go的time.Sleep与time.Ticker

go time.Ticker与time.Sleep

核心实现机制

  • time.Sleep
    通过向 Go 运行时的定时器堆插入一次性定时器实现。
    每次调用会触发定时器注册和销毁,产生以下开销:
    • 堆插入/删除操作(复杂度 O(log n))
    • 上下文切换(Goroutine 休眠/唤醒)
  • time.Ticker
    创建周期性定时器,复用底层时间轮(Time Wheel)资源。
    主要开销包括:
    • 初始化时的定时器注册
    • 周期性的通道事件发送
    • 事件接收时的调度
    • 上下文切换(Goroutine 休眠/唤醒)

误差来源

    • 唤醒:当goroutine因等待ticker.CSleep到期被阻塞时,会进入_Gwaiting状态,并解绑当前的M(系统线程)。
    • 定时器触发:时间到达后,调度器将goroutine标记为可运行(_Grunnable),并将其加入目标P的本地运行队列。
    • 队列满载时的处理:如果P的本地队列已满(默认容量为256),goroutine会被转移到全局队列。
  1. 时间误差来源

    • 全局队列调度延迟:全局队列的获取需要全局锁,高并发时竞争激烈,导致goroutine调度延迟。
    • 工作窃取(Work Stealing)延迟:空闲的P会从全局队列或其他P的本地队列窃取任务,但这一过程非实时。
  2. 误差公式

    实际误差 = 理论间隔 + 全局队列调度延迟 + 上下文切换耗时
    

    其中:

    • 全局队列调度延迟:通常为微秒级(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

posted @ 2025-03-26 23:10  aliliusi  阅读(181)  评论(0)    收藏  举报