定时器底层机制
第一部分:定时器的实现原理
一般定时器的实现方式
定时器是操作系统和编程语言中的重要组件,用于在指定时间后执行特定操作。常见的定时器实现方式包括:
- 链表方式:将所有定时器按触发时间排序成链表,每次时钟中断时检查链表头部
- 时间轮算法:类似钟表,将时间分为多个槽位,定时器分布在不同槽位中
- 最小堆算法:使用优先队列(最小堆)存储定时器,保证最早触发的定时器在堆顶
- 红黑树算法:使用平衡二叉搜索树存储定时器,保证插入、删除、查找的时间复杂度
Go语言定时器的设计
Go语言的定时器实现基于最小堆算法,具有以下特点:
- 每个P都有独立的定时器堆:避免全局锁竞争,提高并发性能
- 四叉堆结构:相比二叉堆,减少了比较次数,提高cache命中率
- 延迟调整机制:定时器修改时不立即调整堆结构,而是标记后批量处理
- 与netpoll集成:利用操作系统的IO多路复用机制实现高效等待
核心数据结构:
type timer struct {
when int64 // 触发时间(纳秒时间戳)
period int64 // 重复间隔(0表示一次性定时器)
f func() // 触发时执行的函数
// ... 其他字段
}
type timers struct {
heap []timerWhen // 四叉最小堆
// ... 管理字段
}
算法优势:
- 插入/删除时间复杂度:O(log N)
- 获取最近定时器:O(1)
- 内存局部性好,cache友好
第二部分:Go定时器的基本使用
示例程序分析
让我们通过一个完整的示例来了解Go语言定时器的基本用法:
package main
import (
"fmt"
"time"
)
// 格式化当前时间
func formatNow() string {
return time.Now().Format("15:04:05")
}
// 演示 time.Sleep 的使用
func demoSleep() {
fmt.Println("\n=== time.Sleep 示例 ===")
fmt.Printf("开始休眠 2 秒... %s\n", formatNow())
time.Sleep(2 * time.Second)
fmt.Printf("休眠结束: %s\n", formatNow())
}
// 演示 time.After 的使用
func demoAfter() {
fmt.Println("\n=== time.After 示例 ===")
fmt.Printf("创建 time.After,等待 2 秒... %s\n", formatNow())
<-time.After(2 * time.Second)
fmt.Printf("time.After 结束: %s\n", formatNow())
}
// 演示 time.NewTimer 的使用
func demoNewTimer() {
fmt.Println("\n=== time.NewTimer 示例 ===")
timer := time.NewTimer(2 * time.Second)
defer timer.Stop() // 确保资源释放
fmt.Printf("定时器已创建,等待 2 秒... %s\n", formatNow())
go func() {
<-timer.C
fmt.Printf("定时器触发: %s\n", formatNow())
}()
}
// 演示 time.AfterFunc 的使用
func demoAfterFunc() {
fmt.Println("\n=== time.AfterFunc 示例 ===")
fmt.Printf("设置 AfterFunc,将在 4 秒后执行 %s\n", formatNow())
timer := time.AfterFunc(4*time.Second, func() {
fmt.Printf("AfterFunc 执行: %s\n", formatNow())
})
defer timer.Stop() // 确保资源释放
}
定时器类型和实现原理
Go语言提供的定时器功能本质上都基于底层的timer结构实现:
-
time.Sleep():同步阻塞当前goroutine
- 内部创建一次性定时器
- 当前goroutine进入等待状态
- 定时器触发后唤醒goroutine
-
time.After():返回一个只读通道
- 创建一次性定时器和通道
- 定时器触发时向通道发送当前时间
- 通过通道接收实现异步等待
-
time.NewTimer():创建可控制的定时器
- 返回Timer结构体,包含通道和控制方法
- 支持Stop()和Reset()操作
- 更灵活的定时器管理
-
time.AfterFunc():定时执行回调函数
- 创建定时器并指定回调函数
- 定时器触发时直接执行函数
- 不需要通道通信
-
time.Ticker:周期性定时器
- period > 0的timer实现
- 每次触发后自动重置下次触发时间
- 用于周期性任务
第三部分:Sleep机制源码深入分析
3.1 timeSleep函数核心逻辑
time.Sleep()是Go语言中最常用的阻塞函数,让我们从源码角度深入理解其实现机制:
// timeSleep 使当前 goroutine 至少休眠 ns 纳秒。
//
//go:linkname timeSleep time.Sleep
func timeSleep(ns int64) {
// 如果睡眠时间小于等于0,直接返回,不进行任何操作
if ns <= 0 {
return
}
// 获取当前运行的goroutine
gp := getg()
// 获取或创建goroutine专属的定时器
// 每个goroutine都有一个复用的timer,避免频繁分配
t := gp.timer
if t == nil {
// 第一次使用时创建定时器
t = new(timer)
// 初始化定时器,设置回调函数为goroutineReady,参数为当前goroutine
t.init(goroutineReady, gp)
// 保存到goroutine结构中,下次复用
gp.timer = t
}
// 计算唤醒时间:当前时间 + 睡眠时长
when := nanotime() + ns
// 检查时间溢出,防止整数溢出导致的错误
if when < 0 { // check for overflow.
when = maxWhen // 设置为最大值
}
// 保存唤醒时间到goroutine结构中
// 这个字段在resetForSleep中会被使用
gp.sleepWhen = when
// 调用gopark将当前goroutine置为等待状态
// resetForSleep: 在goroutine被挂起后调用的函数
// nil: 锁参数(这里不需要锁)
// waitReasonSleep: 等待原因,用于调试和统计
// traceBlockSleep: 追踪事件类型
// 1: 跳过的栈帧数
gopark(resetForSleep, nil, waitReasonSleep, traceBlockSleep, 1)
}
关键设计点:
- 定时器复用:每个goroutine复用一个timer,避免频繁分配内存
- 两阶段操作:先挂起goroutine,再设置定时器,避免竞态条件
- 溢出保护:防止时间计算溢出导致的异常行为
3.2 timer初始化机制
// init 初始化新分配的定时器 t。
// 任何分配定时器的代码都必须在使用之前调用 t.init。
// arg 和 f 可以在 init 期间设置,或者它们可以在 init 中为 nil,
// 并通过未来对 t.modify 的调用来设置。
func (t *timer) init(f func(arg any, seq uintptr, delay int64), arg any) {
// 初始化互斥锁,设置锁的等级为 lockRankTimer
// 这是Go运行时锁排序机制的一部分,防止死锁
lockInit(&t.mu, lockRankTimer)
// 设置定时器触发时的回调函数
// 对于Sleep操作,这个函数是goroutineReady
t.f = f
// 设置传递给回调函数的参数
// 对于Sleep操作,这个参数是当前的goroutine指针
t.arg = arg
}
3.3 gopark机制详解
gopark是Go调度器的核心函数,用于将goroutine从运行状态转换为等待状态:
// gopark将当前goroutine置为等待状态并调用unlockf函数
// 如果unlockf返回false,goroutine会恢复运行
func gopark(unlockf func(*g, unsafe.Pointer) bool, lock unsafe.Pointer,
reason waitReason, traceReason traceBlockReason, traceskip int) {
// 获取当前goroutine和关联的m(machine)
mp := acquirem()
gp := mp.curg
// 记录调用位置信息,用于调试和性能分析
status := readgstatus(gp)
if status != _Grunning && status != _Gscanrunning {
throw("gopark: bad g status")
}
// 设置等待原因,这个信息在goroutine dump时会显示
mp.waitlock = lock
mp.waitunlockf = unlockf
gp.waitreason = reason
// 切换到系统栈执行park_m
// 这样做是为了避免在用户栈上执行调度逻辑
releasem(mp)
mcall(park_m)
}
// park_m是gopark的实际执行函数,运行在系统栈上
func park_m(gp *g) {
// 将goroutine状态从_Grunning改为_Gwaiting
casgstatus(gp, _Grunning, _Gwaiting)
// 断开当前m和goroutine的关联
dropg()
// 如果指定了unlock函数,则调用它
// 对于timeSleep,这个函数是resetForSleep
if fn := gp.m.waitunlockf; fn != nil {
ok := fn(gp, gp.m.waitlock)
gp.m.waitunlockf = nil
gp.m.waitlock = nil
if !ok {
// unlock函数返回false,恢复goroutine运行
casgstatus(gp, _Gwaiting, _Grunnable)
execute(gp, true)
}
}
// 调度其他goroutine运行
schedule()
}
3.4 resetForSleep函数分析
// resetForSleep 在 goroutine 为 timeSleep 挂起后调用。
// 我们不能在 timeSleep 中调用 timer.reset,因为如果这是一个短暂的
// 休眠并且有很多 goroutine,那么 P 可能会在 goroutine 挂起之前
// 运行定时器函数 goroutineReady。
func resetForSleep(gp *g, _ unsafe.Pointer) bool {
// 设置定时器的触发时间和重复间隔
// gp.sleepWhen: 在timeSleep中计算的唤醒时间
// 0: 重复间隔为0,表示这是一次性定时器
gp.timer.reset(gp.sleepWhen, 0)
return true // 返回true表示继续park goroutine
}
为什么要分成两个步骤?
这种设计避免了一个重要的竞态条件:
- 如果在
timeSleep中直接设置定时器,可能定时器在goroutine被挂起之前就触发了 - 这会导致
goroutineReady试图唤醒一个还在运行的goroutine,造成状态混乱 - 通过先挂起goroutine,再设置定时器,确保了状态的一致性
3.5 timer.reset和timer.modify详解
// reset 重置定时器应该触发的时间。
// 如果用于非活动定时器,定时器将变为活动状态。
// 报告定时器是否活动并被停止。
func (t *timer) reset(when, period int64) bool {
// reset实际上是modify的简化版本,不改变回调函数和参数
return t.modify(when, period, nil, nil, 0)
}
3.5.1 async和isChan参数详解
在深入理解 modify 函数之前,我们需要先理解两个关键参数。
**重要说明:**在Go 1.23.8版本中,debug.asynctimerchan默认为0(不开启),所以async参数通常为false。为了简化理解,我们先重点分析isChan参数的影响。
1. isChan参数 - 通道定时器标识
// timer结构体中的字段
isChan bool // timer has a channel; immutable; can be read without lock
isChan 标识这个定时器是否关联了通道,这是最重要的区分标准:
isChan = false(函数定时器)
- 使用场景:
time.Sleep()、time.AfterFunc() - 行为特点:
- 定时器触发时直接执行回调函数
- 不需要
sendLock保护(没有通道发送操作) - 不使用序列号机制
- 处理逻辑简单,性能较好
- 不涉及复杂的并发同步
// 函数定时器的典型处理流程
func (t *timer) modify(...) bool {
async := debug.asynctimerchan.Load() != 0 // 通常为false
// 函数定时器跳过这个分支
if !async && t.isChan {
// 跳过:不需要sendLock
}
t.lock()
// ... 核心逻辑相同 ...
// 函数定时器跳过这个分支
if !async && t.isChan {
// 跳过:不需要序列号和通道清理
}
t.unlock()
return pending
}
通道定时器modify的关键差异:
- 双锁机制:
sendLock+t.mu,确保发送操作的原子性 - 序列号递增:每次modify都递增
t.seq,使正在进行的发送无效 - 通道清理:
timerchandrain清空过时的时间值 - 发送状态检查:通过
t.isSending检测并发的发送操作
4.5 sendTime函数与通道发送机制
虽然无法直接查看time包的sendTime源码,但根据runtime中的调用逻辑,我们可以分析其工作原理:
// sendTime的预期实现逻辑(基于runtime调用模式)
func sendTime(c any, seq uintptr, delta int64) {
// delta是延迟时间:nanotime() - t.when
// 表示定时器应该触发的时间与实际触发时间的差值
// 计算应该发送的时间值
// 减去delta,使其看起来像在正确的时间发送
timeToSend := time.Now().Add(time.Duration(-delta))
// 非阻塞发送到通道
// 如果通道已满或没有接收者,发送会失败但不阻塞
select {
case c.(chan time.Time) <- timeToSend:
// 发送成功
default:
// 发送失败,但这是预期的行为
// 避免阻塞定时器处理goroutine
}
}
sendTime的设计要点:
- 延迟补偿:通过
delta参数补偿处理延迟,确保时间值的准确性 - 非阻塞发送:使用
select避免阻塞定时器处理流程 - 容错设计:发送失败不影响定时器系统的整体运行
4.6 通道定时器的触发流程
通道定时器的触发比函数定时器复杂得多,涉及序列号检查和并发控制:
flowchart TD
A["调度器检查到期定时器"] --> B["timer.unlockAndRun(now)"]
B --> C["检查 t.isChan"]
C -->|true| D["通道定时器处理"]
C -->|false| E["函数定时器处理"]
D --> F["保存当前序列号 seq"]
F --> G["设置 t.isSending++"]
G --> H["释放定时器锁"]
H --> I["获取 sendLock"]
I --> J["重新获取定时器锁"]
J --> K["检查序列号是否变化"]
K -->|seq != t.seq| L["序列号已变化"]
K -->|seq == t.seq| M["序列号未变化"]
L --> N["跳过发送(过时操作)"]
M --> O["执行 sendTime(c, seq, delta)"]
N --> P["设置 t.isSending--"]
O --> P
P --> Q["释放所有锁"]
E --> R["直接执行回调函数"]
R --> S["goroutineReady(gp)"]
style D fill:#ffeb3b,color:#000
style K fill:#ff9800,color:#fff
style L fill:#f44336,color:#fff
style M fill:#4caf50,color:#fff
4.7 序列号机制的深度分析
序列号机制是通道定时器最复杂也最关键的部分,它解决了多个并发问题:
// 序列号机制的核心逻辑(在unlockAndRun中)
func (t *timer) unlockAndRun(now int64) {
if t.isChan {
// === 第一阶段:准备发送 ===
seq := t.seq // 保存当前序列号
t.isSending.Add(1) // 标记正在发送
// 释放定时器锁,避免在发送时持有锁
t.unlock()
// === 第二阶段:获取发送锁 ===
lock(&t.sendLock)
// === 第三阶段:重新检查状态 ===
t.lock()
// 关键检查:序列号是否在此期间被修改
if t.seq != seq {
// 序列号已变化,说明定时器被Stop/Reset
// 将回调函数替换为空函数,跳过发送
f = func(any, uintptr, int64) {}
}
// === 第四阶段:执行发送 ===
// 即使是空函数也会执行,确保isSending计数正确
f(t.arg, seq, delay)
// === 第五阶段:清理 ===
t.isSending.Add(-1) // 标记发送完成
unlock(&t.sendLock)
}
}
序列号机制解决的竞态条件:
-
Stop/Reset竞争:
时间线: T1: unlockAndRun开始,保存seq=5 T2: 用户调用timer.Stop(),seq递增为6 T3: unlockAndRun检查发现seq不匹配,跳过发送 结果:避免向已停止定时器的通道发送值 -
并发Reset竞争:
时间线: T1: unlockAndRun(定时器A)开始 T2: 用户Reset定时器为新时间 T3: unlockAndRun检查序列号,发现已变化 结果:不会发送基于旧配置的时间值 -
通道满竞争:
时间线: T1: 定时器准备发送 T2: 通道已满,无法发送 T3: 用户Reset定时器 结果:序列号机制确保不会有过时值残留
4.8 NewTimer完整调用链分析
从用户代码到底层实现,NewTimer的完整调用链如下:
sequenceDiagram
participant User as 用户代码
participant Time as time包
participant Runtime as runtime包
participant Heap as 定时器堆
participant Sched as 调度器
User->>Time: timer := time.NewTimer(5*time.Second)
Time->>Time: 创建 chan Time
Time->>Runtime: runtime.newTimer(when, 0, sendTime, ch, 0)
Runtime->>Runtime: 分配 timeTimer 结构
Runtime->>Runtime: t.isChan = true
Runtime->>Runtime: t.arg = ch (通道)
Runtime->>Runtime: t.f = sendTime (发送函数)
Runtime->>Runtime: t.modify(when, 0, f, arg, seq)
Runtime->>Runtime: 获取当前P的定时器堆
Runtime->>Heap: 将定时器插入堆中
Runtime->>Runtime: wakeNetPoller() 唤醒网络轮询器
Note over Runtime,Heap: 定时器在堆中等待触发
Sched->>Heap: 调度器定期检查到期定时器
Heap->>Runtime: 发现定时器到期
Runtime->>Runtime: unlockAndRun(now)
Runtime->>Runtime: 检查 t.isChan == true
Runtime->>Runtime: 保存序列号,设置 isSending
Runtime->>Time: 调用 sendTime(ch, seq, delta)
Time->>Time: 非阻塞发送时间值到通道
User->>Time: timeValue := <-timer.C
Time-->>User: 返回触发时间
4.9 NewTimer与Sleep的性能对比
基于源码分析,我们可以总结两种机制的性能特征:
| 性能维度 | time.Sleep() | time.NewTimer() |
|---|---|---|
| 内存分配 | 仅分配timer结构 | timer结构 + 通道 + 通道缓冲区 |
| 锁竞争 | 单一定时器锁 | 定时器锁 + sendLock双锁 |
| 触发延迟 | 直接唤醒goroutine | 通道发送 + 接收开销 |
| 并发安全 | 简单的状态检查 | 复杂的序列号机制 |
| 垃圾回收 | 定时器回收 | 定时器 + 通道回收 |
| 适用场景 | 简单延迟等待 | 需要取消/重置的定时 |
性能测试示例:
// 性能对比测试
func BenchmarkSleep(b *testing.B) {
for i := 0; i < b.N; i++ {
time.Sleep(time.Microsecond)
}
}
func BenchmarkNewTimer(b *testing.B) {
for i := 0; i < b.N; i++ {
timer := time.NewTimer(time.Microsecond)
<-timer.C
timer.Stop() // 清理资源
}
}
// 典型结果(仅供参考):
// BenchmarkSleep-8 1000000 1200 ns/op 0 allocs/op
// BenchmarkNewTimer-8 500000 2400 ns/op 2 allocs/op
4.10 NewTimer最佳实践
基于源码分析,以下是使用NewTimer的最佳实践:
4.10.1 资源管理
// ✅ 正确:及时停止定时器
func goodExample() {
timer := time.NewTimer(5 * time.Second)
defer timer.Stop() // 确保资源释放
select {
case <-timer.C:
// 定时器触发
case <-ctx.Done():
// 上下文取消
return
}
}
// ❌ 错误:忘记停止定时器
func badExample() {
timer := time.NewTimer(5 * time.Second)
// 忘记调用 timer.Stop()
<-timer.C // 可能导致资源泄漏
}
4.10.2 重置操作
// ✅ 正确:安全的重置模式
func safeReset(timer *time.Timer, d time.Duration) {
if !timer.Stop() {
// 定时器已经触发,清空通道
select {
case <-timer.C:
default:
}
}
timer.Reset(d)
}
// ❌ 错误:直接重置可能导致竞态
func unsafeReset(timer *time.Timer, d time.Duration) {
timer.Reset(d) // 可能与正在进行的发送竞争
}
4.10.3 性能优化
// ✅ 推荐:复用定时器
type TimerPool struct {
pool sync.Pool
}
func (p *TimerPool) Get(d time.Duration) *time.Timer {
if timer, ok := p.pool.Get().(*time.Timer); ok {
timer.Reset(d)
return timer
}
return time.NewTimer(d)
}
func (p *TimerPool) Put(timer *time.Timer) {
if timer.Stop() {
p.pool.Put(timer)
}
}
4.11 总结
NewTimer机制相比Sleep具有以下特点:
优势:
- 灵活性:支持Stop/Reset操作
- 可组合性:可与select语句配合使用
- 并发安全:通过序列号机制保证正确性
代价:
- 复杂性:双锁机制和序列号检查
- 性能开销:额外的内存分配和通道操作
- 资源管理:需要手动停止以避免泄漏
适用场景:
- 需要取消的定时操作
- 超时控制机制
- 复杂的定时逻辑
通过深入理解NewTimer的实现机制,我们可以更好地在实际项目中选择合适的定时方案,并避免常见的性能陷阱和资源泄漏问题。
isChan = true(通道定时器)
- 使用场景:
time.After()、time.NewTimer()、time.NewTicker() - 行为特点:
- 定时器触发时向通道发送时间值
- 需要
sendLock保护通道发送操作 - 使用序列号
t.seq防止过时的通道发送 - 需要处理复杂的并发同步问题
- 要协调定时器状态与通道状态
// 通道定时器的典型处理流程
func (t *timer) modify(...) bool {
async := debug.asynctimerchan.Load() != 0 // 通常为false
// 通道定时器需要获取发送锁
if !async && t.isChan {
lock(&t.sendLock) // 保护通道发送操作
}
t.lock()
// ... 核心逻辑相同 ...
// 通道定时器需要序列号保护
if !async && t.isChan {
t.seq++ // 递增序列号,使过时发送无效
// 处理正在发送的特殊情况
if oldPeriod == 0 && t.isSending.Load() > 0 {
pending = true
}
}
t.unlock()
// 通道定时器需要清理过时值
if !async && t.isChan {
if timerchandrain(t.hchan()) { // 清空通道中的过时值
pending = true
}
unlock(&t.sendLock)
}
return pending
}
2. async参数 - 异步定时器模式(简化说明)
// 检查是否为异步定时器通道模式
async := debug.asynctimerchan.Load() != 0 // Go 1.23.8默认为0
由于默认不开启,实际运行中主要是async = false的同步模式:
- Go 1.23之前:只有同步模式
- Go 1.23+:引入异步模式优化,但默认关闭
- 异步模式主要优化通道定时器的性能,减少锁竞争
关键区别总结:
| 特性 | 函数定时器 (isChan=false) | 通道定时器 (isChan=true) |
|---|---|---|
| 使用场景 | Sleep, AfterFunc | After, NewTimer, NewTicker |
| 发送锁 | 不需要 | 需要sendLock |
| 序列号 | 不使用 | 使用t.seq防止过时发送 |
| 通道清理 | 不涉及 | 需要timerchandrain |
| 复杂度 | 简单 | 复杂(并发同步) |
| 性能 | 较好 | 相对较低(但更安全) |
3.5.2 maybeRunAsync函数的作用
// maybeRunAsync 检查 t 是否需要被触发并在需要时运行它。
func (t *timer) maybeRunAsync() {
assertLockHeld(&t.mu)
// 只有通道定时器且不在堆中但应该已经触发的情况下才执行
if t.state&timerHeaped == 0 && t.isChan && t.when > 0 {
// 如果定时器应该已经触发(但还没有任何东西查看它)
// 立即触发,确保接收者能看到正确的值
if now := nanotime(); t.when <= now {
systemstack(func() {
t.unlockAndRun(now) // 立即运行定时器
})
t.lock() // 重新锁定
}
}
}
3.5.5 序列号机制的深入理解
同步通道定时器使用序列号机制来解决复杂的竞态条件问题:
// 定时器结构中的序列号字段
seq uintptr // 序列号
// 在modify中递增序列号
t.seq++
// 在unlockAndRun中检查序列号
if t.seq != seq {
f = func(any, uintptr, int64) {} // 替换为空函数,跳过发送
}
序列号机制的工作原理:
- 获取序列号:在准备发送前,保存当前序列号
- 检查序列号:在实际发送前,比较序列号是否变化
- 跳过过时发送:如果序列号不匹配,跳过发送操作
解决的竞态条件场景:
// 场景:定时器即将触发时被reset
// 时间线:
// T1: timer.unlockAndRun 准备发送,保存 seq=5
// T2: timer.stop/reset 调用,递增 seq=6
// T3: unlockAndRun 检查发现 seq 不匹配,跳过发送
// 结果:避免了向已重置定时器的通道发送过时值
3.5.6 性能对比分析
不同模式组合的性能特征:
| 模式组合 | 锁开销 | 内存开销 | CPU开销 | 适用场景 |
|---|---|---|---|---|
| 同步函数定时器 | 低 | 低 | 低 | Sleep, AfterFunc |
| 异步函数定时器 | 低 | 低 | 最低 | 高性能Sleep |
| 同步通道定时器 | 高 | 中 | 高 | 兼容性要求高 |
| 异步通道定时器 | 低 | 低 | 中 | 高并发通道定时器 |
优化建议:
- Go 1.23+ 建议启用 async 模式提升性能
- 优先使用函数定时器而非通道定时器(当不需要select时)
- 避免频繁的 stop/reset 操作
3.6 定时器添加逻辑:needsAdd和maybeAdd
// t must be locked.
// needsAdd 报告 t 是否需要添加到定时器堆中。
// t 必须被锁定。
func (t *timer) needsAdd() bool {
assertLockHeld(&t.mu)
// 需要添加到堆的条件:
// 1. 定时器不在堆中 (timerHeaped == 0)
// 2. 有设置触发时间 (t.when > 0)
// 3. 不是通道定时器,或者是通道定时器但有goroutine在等待 (!t.isChan || t.blocked > 0)
need := t.state&timerHeaped == 0 && t.when > 0 && (!t.isChan || t.blocked > 0)
if need {
t.trace("needsAdd+")
} else {
t.trace("needsAdd-")
}
return need
}
// maybeAdd 如果 t 需要在堆中,则将 t 添加到本地定时器堆中。
// 调用者不得持有 t 的锁或任何定时器堆锁。
func (t *timer) maybeAdd() {
// 重要:为了避免锁顺序问题,这个函数不能在持有t的锁时调用
// 因为timers.adjust会先锁定ts.lock再锁定各个timer的锁
// 防止当前goroutine被重新调度到不同的P上
// 确保定时器被添加到当前P的定时器堆中
mp := acquirem()
ts := &mp.p.ptr().timers // 获取当前P的定时器集合
ts.lock() // 锁定定时器集合
ts.cleanHead() // 清理堆顶的无效定时器
t.lock() // 锁定要添加的定时器
t.trace("maybeAdd")
when := int64(0)
wake := false
// 再次检查是否需要添加(因为状态可能已改变)
if t.needsAdd() {
t.state |= timerHeaped // 标记定时器在堆中
when = t.when
// 检查是否需要唤醒网络轮询器
wakeTime := ts.wakeTime()
wake = wakeTime == 0 || when < wakeTime
// 将定时器添加到堆中
ts.addHeap(t)
}
t.unlock()
ts.unlock()
releasem(mp)
// 如果需要,唤醒网络轮询器
if wake {
wakeNetPoller(when)
}
}
3.7 堆操作核心逻辑
Go定时器系统使用四叉最小堆来管理定时器。四叉最小堆需要满足以下算法条件:
-
堆性质(Heap Property):对于任意节点i,其值必须小于等于所有子节点的值
heap[i].when ≤ heap[4*i+1].when(第1个子节点)heap[i].when ≤ heap[4*i+2].when(第2个子节点)heap[i].when ≤ heap[4*i+3].when(第3个子节点)heap[i].when ≤ heap[4*i+4].when(第4个子节点)
-
完全四叉树结构(Complete 4-ary Tree):除最后一层外,所有层都必须被完全填满,最后一层从左到右连续填充
-
索引关系:
- 父节点索引:
parent(i) = (i-1)/4 - 子节点索引:
children(i) = [4*i+1, 4*i+2, 4*i+3, 4*i+4]
- 父节点索引:
-
最小值保证:堆顶元素
heap[0]始终是所有元素中的最小值,即最早需要触发的定时器
让我们详细分析每个核心操作的源码实现。
3.7.1 堆结构基础知识
四叉堆公式:
const timerHeapN = 4 // 四叉堆的分支因子
// 父节点索引计算
parentIndex = (i - 1) / timerHeapN
// 子节点索引计算
firstChild = timerHeapN * i + 1
children = [firstChild, firstChild+1, firstChild+2, firstChild+3]
数据结构:
type timerWhen struct {
timer *timer // 定时器指针
when int64 // 触发时间
}
type timers struct {
heap []timerWhen // 四叉最小堆数组
// ... 其他字段
}
3.7.2 addHeap - 添加定时器到堆
操作前状态模拟:
graph TD
A["[0] 10ns<br/>堆顶"] --> B["[1] 20ns"]
A --> C["[2] 30ns"]
A --> D["[3] 40ns"]
A --> E["[4] 50ns"]
B --> F["[5] 25ns"]
B --> G["[6] 35ns"]
B --> H["[7] 60ns"]
B --> I["[8] 45ns"]
style A fill:#e1f5fe
style B fill:#f3e5f5
style C fill:#f3e5f5
style D fill:#f3e5f5
style E fill:#f3e5f5
style F fill:#fff3e0
style G fill:#fff3e0
style H fill:#fff3e0
style I fill:#fff3e0
classDef heap fill:#e8f5e8,stroke:#4caf50,stroke-width:2px
class A,B,C,D,E,F,G,H,I heap
现在要添加: when=15ns 的新定时器
addHeap源码分析:
// addHeap adds t to the timers heap.
// The caller must hold ts.lock or the world must be stopped.
// The caller must also have checked that t belongs in the heap.
// addHeap 将 t 添加到定时器堆中。
// 调用者必须持有 ts.lock 或世界必须停止。
// 调用者还必须检查 t 是否属于堆。
func (ts *timers) addHeap(t *timer) {
assertWorldStoppedOrLockHeld(&ts.mu)
// 步骤1: 确保网络轮询器已初始化
// 定时器依赖网络轮询器来实现高效等待
if netpollInited.Load() == 0 {
netpollGenericInit()
}
// 步骤2: 验证定时器状态
if t.ts != nil {
throw("ts set in timer")
}
// 步骤3: 建立定时器和定时器集合的关联
t.ts = ts
// 步骤4: 将定时器添加到堆的末尾
ts.heap = append(ts.heap, timerWhen{t, t.when})
// 步骤5: 向上调整,维护堆的性质
ts.siftUp(len(ts.heap) - 1)
// 步骤6: 如果新添加的定时器成为了堆顶,更新最小唤醒时间
if t == ts.heap[0].timer {
ts.updateMinWhenHeap()
}
}
addHeap操作流程图:
flowchart TD
A["步骤1: 添加到堆末尾<br/>[10,20,30,40,50,25,35,60,45,15]"]
A --> B["步骤2: 调用siftUp(9)"]
B --> C["比较15ns与父节点<br/>父节点索引: (9-1)/4 = 2<br/>heap[2] = 30ns"]
C --> D["15ns < 30ns<br/>需要交换"]
D --> E["继续向上比较<br/>父节点索引: (2-1)/4 = 0<br/>heap[0] = 10ns"]
E --> F["15ns >= 10ns<br/>停止调整"]
F --> G["最终位置: [2]"]
style A fill:#e3f2fd
style B fill:#f3e5f5
style C fill:#fff3e0
style D fill:#ffebee
style E fill:#fff3e0
style F fill:#e8f5e8
style G fill:#e1f5fe
操作后状态:
graph TD
A["[0] 10ns<br/>堆顶"] --> B["[1] 20ns"]
A --> C["[2] 15ns<br/>新位置"]
A --> D["[3] 40ns"]
A --> E["[4] 50ns"]
B --> F["[5] 25ns"]
B --> G["[6] 35ns"]
B --> H["[7] 50ns"]
B --> I["[8] 45ns"]
C --> J["[9] 30ns<br/>被下移"]
style A fill:#4caf50,color:#fff
style C fill:#ff9800,color:#fff
style J fill:#2196f3,color:#fff
style B fill:#f3e5f5
style D fill:#f3e5f5
style E fill:#ffebee
style F fill:#fff3e0
style G fill:#fff3e0
style H fill:#fff3e0
style I fill:#fff3e0
%% 注意:在实际四叉堆中,节点[2]还可以有子节点[10],[11],[12]
%% 但在当前示例中,堆只有10个元素,所以这些位置暂未使用
style A fill:#e1f5fe
style C fill:#4caf50,color:#fff
style J fill:#ff9800,color:#fff
style B fill:#f3e5f5
style D fill:#f3e5f5
style E fill:#ffebee
style F fill:#fff3e0
style G fill:#fff3e0
style H fill:#fff3e0
style I fill:#fff3e0
堆性质验证: ✓ 每个父节点都小于等于其子节点
3.7.3 siftUp - 堆向上调整
加在数组后面,需要往上找合适的位置
siftUp源码分析:
// siftUp puts the timer at position i in the right place
// in the heap by moving it up toward the top of the heap.
// siftUp 通过将位置 i 的定时器向堆顶移动,
// 将其放在堆中的正确位置。
func (ts *timers) siftUp(i int) {
heap := ts.heap
if i >= len(heap) {
badTimer() // 索引越界,数据结构损坏
}
// 步骤1: 获取要调整的定时器和触发时间
tw := heap[i] // 要调整的定时器
when := tw.when // 触发时间
if when <= 0 {
badTimer() // 触发时间无效
}
// 步骤2: 向上调整循环
for i > 0 {
// 四叉堆的父节点计算公式:(i-1)/4
p := int(uint(i-1) / timerHeapN) // parent,timerHeapN = 4
// 步骤3: 比较当前节点与父节点
if when >= heap[p].when {
break // 堆性质已满足,停止调整
}
// 步骤4: 将父节点下移
heap[i] = heap[p]
i = p
}
// 步骤5: 将原始节点放到最终位置(如果位置有变化)
if heap[i].timer != tw.timer {
heap[i] = tw
}
}
siftUp详细执行过程:
flowchart TD
Start["开始 siftUp(9)<br/>when=15ns"]
Start --> Check1["循环1: i=9<br/>父节点 p=(9-1)/4=2<br/>heap[2].when=30ns"]
Check1 --> Compare1["比较: 15ns < 30ns?"]
Compare1 --> |"是"| Move1["移动: heap[9]=heap[2]<br/>i=2"]
Move1 --> Check2["循环2: i=2<br/>父节点 p=(2-1)/4=0<br/>heap[0].when=10ns"]
Check2 --> Compare2["比较: 15ns >= 10ns?"]
Compare2 --> |"是"| Stop["停止循环"]
Stop --> Place["放置: heap[2]=tw<br/>15ns放入位置2"]
Compare1 --> |"否"| Stop
Compare2 --> |"否"| Move2["继续向上移动"]
style Start fill:#e3f2fd
style Check1 fill:#fff3e0
style Compare1 fill:#ffebee
style Move1 fill:#e8f5e8
style Check2 fill:#fff3e0
style Compare2 fill:#ffebee
style Stop fill:#4caf50,color:#fff
style Place fill:#2196f3,color:#fff
siftUp(9) 调整 when=15ns 的定时器过程:
3.7.4 siftDown - 堆向下调整
操作前状态模拟: 堆顶移掉,直接交换最后元素(30ns)到堆顶,需要往下找合适位置
graph TD
A["[0] 30ns<br/>需要向下调整"] --> B["[1] 20ns"]
A --> C["[2] 15ns"]
A --> D["[3] 40ns"]
A --> E["[4] 50ns"]
B --> F["[5] 25ns"]
B --> G["[6] 35ns"]
B --> H["[7] 50ns"]
B --> I["[8] 45ns"]
style A fill:#ff9800,color:#fff
style C fill:#4caf50,color:#fff
style B fill:#f3e5f5
style D fill:#f3e5f5
style E fill:#ffebee
style F fill:#fff3e0
style G fill:#fff3e0
style H fill:#fff3e0
style I fill:#fff3e0
siftDown源码分析:
// siftDown puts the timer at position i in the right place
// in the heap by moving it down toward the bottom of the heap.
// siftDown 通过将位置 i 的定时器向堆底移动,
// 将其放在堆中的正确位置。
func (ts *timers) siftDown(i int) {
heap := ts.heap
n := len(heap)
if i >= n {
badTimer()
}
if i*timerHeapN+1 >= n {
return // 没有子节点,无需调整
}
// 步骤1: 获取要调整的定时器信息
tw := heap[i]
when := tw.when
if when <= 0 {
badTimer()
}
// 步骤2: 向下调整循环
for {
// 步骤3: 计算第一个子节点位置
leftChild := i*timerHeapN + 1
if leftChild >= n {
break // 没有子节点,停止调整
}
// 步骤4: 在所有子节点中找到最小值
w := when
c := -1
for j, tw := range heap[leftChild:min(leftChild+timerHeapN, n)] {
if tw.when < w {
w = tw.when
c = leftChild + j
}
}
// 步骤5: 如果当前节点已经是最小的,停止调整
if c < 0 {
break
}
// 步骤6: 将最小子节点上移
heap[i] = heap[c]
i = c
}
// 步骤7: 将原始节点放到最终位置
if heap[i].timer != tw.timer {
heap[i] = tw
}
}
siftDown详细执行过程:
flowchart TD
Start["开始 siftDown(0)<br/>when=30ns"]
Start --> FindMin["查找子节点最小值<br/>子节点: [1,2,3,4]<br/>值: [20,15,40,空]"]
FindMin --> MinFound["最小值: heap[2]=15ns<br/>索引 c=2"]
MinFound --> Compare["比较: 30ns > 15ns?"]
Compare --> |"是"| Move1["交换: heap[0]=heap[2]<br/>i=2"]
Move1 --> Check["检查 i=2 是否还有子节点<br/>leftChild = 2×4+1 = 9"]
Check --> |"有子节点"| FindMin2["查找子节点最小值<br/>只有[9]=30ns"]
FindMin2 --> Compare2["比较: 30ns >= 30ns?"]
Compare2 --> |"是"| Stop["停止调整"]
Stop --> Place["最终放置: heap[2]=30ns"]
Compare --> |"否"| Stop
Check --> |"无子节点"| Stop
style Start fill:#ff9800,color:#fff
style FindMin fill:#fff3e0
style MinFound fill:#4caf50,color:#fff
style Compare fill:#ffebee
style Move1 fill:#e8f5e8
style Check fill:#fff3e0
style FindMin2 fill:#fff3e0
style Compare2 fill:#ffebee
style Stop fill:#2196f3,color:#fff
style Place fill:#e1f5fe
操作后状态:
graph TD
A["[0] 15ns<br/>新的堆顶"] --> B["[1] 20ns"]
A --> C["[2] 30ns<br/>最终位置"]
A --> D["[3] 40ns"]
A --> E["[4] 50ns"]
B --> F["[5] 25ns"]
B --> G["[6] 35ns"]
B --> H["[7] 50ns"]
B --> I["[8] 45ns"]
style A fill:#4caf50,color:#fff
style C fill:#ff9800,color:#fff
style B fill:#f3e5f5
style D fill:#f3e5f5
style E fill:#ffebee
style F fill:#fff3e0
style G fill:#fff3e0
style H fill:#fff3e0
style I fill:#fff3e0
堆性质验证: ✓ 每个父节点都小于等于其子节点
3.7.5 deleteMin - 删除堆顶定时器
操作前状态模拟:
要删除堆顶定时器 (when=10ns)
deleteMin源码分析:
// deleteMin removes timer 0 from ts.
// ts must be locked.
// deleteMin 从 ts 中删除定时器 0。
// ts 必须被锁定。
func (ts *timers) deleteMin() {
assertLockHeld(&ts.mu)
// 步骤1: 获取要删除的堆顶定时器
t := ts.heap[0].timer
if t.ts != ts {
throw("wrong timers")
}
// 步骤2: 断开定时器与定时器集合的关联
t.ts = nil
// 步骤3: 用最后一个元素替换堆顶(如果堆不只一个元素)
last := len(ts.heap) - 1
if last > 0 {
ts.heap[0] = ts.heap[last]
}
// 步骤4: 清除最后一个位置并缩减堆大小
ts.heap[last] = timerWhen{}
ts.heap = ts.heap[:last]
// 步骤5: 如果堆非空,调整堆顶元素到正确位置
if last > 0 {
ts.siftDown(0)
}
// 步骤6: 更新最小堆时间
ts.updateMinWhenHeap()
// 步骤7: 如果堆为空,清除修改时间标记
if last == 0 {
// 如果没有定时器,那么显然没有 timerModified 定时器。
ts.minWhenModified.Store(0)
}
}
deleteMin操作流程图:
flowchart TD
Start["开始 deleteMin()<br/>要删除堆顶 when=10ns"]
Start --> Disconnect["断开关联 t.ts = nil"]
Disconnect --> GetLast["获取最后元素<br/>last = len(heap) - 1<br/>lastElement = heap[8] = 45ns"]
GetLast --> Replace["用最后元素替换堆顶<br/>heap[0] = heap[8]"]
Replace --> Shrink["缩减堆大小<br/>heap = heap[:8]"]
Shrink --> SiftDown["调用 siftDown(0)<br/>调整新堆顶45ns"]
SiftDown --> UpdateMin["更新最小触发时间<br/>updateMinWhenHeap()"]
style Start fill:#ff5722,color:#fff
style Disconnect fill:#fff3e0
style GetLast fill:#e8f5e8
style Replace fill:#2196f3,color:#fff
style Shrink fill:#ff9800,color:#fff
style SiftDown fill:#9c27b0,color:#fff
style UpdateMin fill:#4caf50,color:#fff
操作后状态:
graph TD
A["[0] 15ns<br/>新的堆顶"] --> B["[1] 20ns"]
A --> C["[2] 30ns<br/>最终位置"]
A --> D["[3] 40ns"]
A --> E["[4] 50ns"]
B --> F["[5] 25ns"]
B --> G["[6] 35ns"]
B --> H["[7] 50ns"]
B --> I["[8] 45ns"]
style A fill:#4caf50,color:#fff
style C fill:#ff9800,color:#fff
style B fill:#f3e5f5
style D fill:#f3e5f5
style E fill:#ffebee
style F fill:#fff3e0
style G fill:#fff3e0
style H fill:#fff3e0
style I fill:#fff3e0
操作后堆状态:
索引: [0, 1, 2, 3, 4, 5, 6, 7]
when: [15, 20, 35, 40, 50, 25, 45, 50]
变化说明:
- 原堆顶 10ns 被删除
- 新堆顶变为 15ns
- 堆大小从 9 减少到 8
- 保持最小堆性质
3.7.6 updateMinWhenHeap - 更新最小触发时间
updateMinWhenHeap源码分析:
// updateMinWhenHeap sets ts.minWhenHeap to ts.heap[0].when.
// The caller must have locked ts or the world must be stopped.
// updateMinWhenHeap 将 ts.minWhenHeap 设置为 ts.heap[0].when。
// 调用者必须已锁定 ts 或世界必须停止。
func (ts *timers) updateMinWhenHeap() {
assertWorldStoppedOrLockHeld(&ts.mu)
if len(ts.heap) == 0 {
// 步骤1: 堆为空,没有定时器
ts.minWhenHeap.Store(0)
} else {
// 步骤2: 堆顶元素就是最早要触发的定时器
ts.minWhenHeap.Store(ts.heap[0].when)
}
}
3.7.7 cleanHead - 清理堆顶无效定时器
操作前状态模拟:
graph TD
A["[0] 10ns<br/>Zombie"] --> B["[1] 20ns<br/>Normal"]
A --> C["[2] 15ns<br/>Modified"]
A --> D["[3] 40ns<br/>Normal"]
A --> E["[4] 25ns<br/>Normal"]
B --> F["[5] 35ns<br/>Normal"]
B --> G["[6] 50ns<br/>Zombie"]
style A fill:#ffebee
style C fill:#fff3e0
style G fill:#ffebee
style B fill:#e8f5e8
style D fill:#e8f5e8
style E fill:#e8f5e8
style F fill:#e8f5e8
当前堆状态 (包含僵尸定时器):
- 索引: [0, 1, 2, 3, 4, 5, 6]
- when: [10, 20, 15, 40, 25, 35, 50]
- 状态: [Z, N, M, N, N, N, Z]
- 说明: Z=Zombie(僵尸), M=Modified(已修改), N=Normal(正常)
cleanHead源码分析:
// cleanHead cleans up the head of the timer queue. This speeds up
// programs that create and delete timers; leaving them in the heap
// slows down heap operations.
// The caller must have locked ts.
// cleanHead 清理定时器队列的头部。这加速了
// 创建和删除定时器的程序;将它们留在堆中
// 会减慢堆操作。
// 调用者必须已锁定 ts。
func (ts *timers) cleanHead() {
ts.trace("cleanHead")
assertLockHeld(&ts.mu)
gp := getg()
for {
if len(ts.heap) == 0 {
return // 堆为空,无需清理
}
// 步骤1: 防止长时间运行阻塞抢占
if gp.preemptStop {
return // 有抢占请求,稍后再清理
}
// 步骤2: 从堆尾删除僵尸定时器(无需调整堆结构)
n := len(ts.heap)
if t := ts.heap[n-1].timer; t.astate.Load()&timerZombie != 0 {
t.lock()
if t.state&timerZombie != 0 {
// 确认是僵尸,执行删除
t.state &^= timerHeaped | timerZombie | timerModified
t.ts = nil
ts.zombies.Add(-1)
ts.heap[n-1] = timerWhen{}
ts.heap = ts.heap[:n-1]
}
t.unlock()
continue
}
// 步骤3: 处理堆顶定时器
t := ts.heap[0].timer
if t.ts != ts {
throw("bad ts")
}
// 步骤4: 快速路径检查
if t.astate.Load()&(timerModified|timerZombie) == 0 {
// 堆顶定时器无需调整
return
}
// 步骤5: 锁定并更新堆顶定时器
t.lock()
updated := t.updateHeap()
t.unlock()
if !updated {
// 实际无需调整
return
}
// 继续循环,可能有更多需要处理的定时器
}
}
cleanHead操作流程图:
flowchart TD
Start["开始 cleanHead()"] --> CheckEmpty{"堆是否为空?"}
CheckEmpty --> |"是"| Return1["直接返回"]
CheckEmpty --> |"否"| CheckPreempt{"有抢占请求?"}
CheckPreempt --> |"是"| Return2["稍后清理"]
CheckPreempt --> |"否"| CheckTail["检查堆尾僵尸定时器"]
CheckTail --> TailZombie{"堆尾是僵尸?"}
TailZombie --> |"是"| RemoveTail["删除堆尾僵尸<br/>heap = heap[:n-1]"]
TailZombie --> |"否"| CheckHead["检查堆顶定时器"]
RemoveTail --> CheckEmpty
CheckHead --> HeadState{"堆顶状态检查"}
HeadState --> |"无需调整"| Return3["清理完成"]
HeadState --> |"需要调整"| UpdateHead["updateHeap()"]
UpdateHead --> Updated{"是否有更新?"}
Updated --> |"是"| CheckEmpty
Updated --> |"否"| Return3
style Start fill:#e3f2fd
style RemoveTail fill:#ffebee
style UpdateHead fill:#fff3e0
style Return3 fill:#4caf50,color:#fff
清理后堆状态 (僵尸定时器已清理):
- 索引: [0, 1, 2, 3, 4]
- when: [15, 20, 25, 40, 35]
- 状态: [N, N, N, N, N]
- 说明: 所有定时器都是正常状态,堆大小从7减少到5
- 变化:
cleanHead() 执行流程:
-
检查堆尾僵尸定时器
- 堆尾检查: [6] 状态=Zombie (本例中堆尾是僵尸节点)
- 锁定定时器[6] → 确认僵尸状态
- 清理:
state &^= timerHeaped|timerZombie|timerModified - 断开:
t.ts = nil→ 计数:zombies.Add(-1) - 删除:
heap = heap[:n-1](堆大小从7减少到6) - 继续循环检查新的堆尾[5]
-
处理堆顶定时器[0]
- 状态检查:
astate = timerZombie - 锁定定时器[0] → 调用
updateHeap() - 检查
timerZombie标志 → 清理状态位 - 调用
deleteMin()→ 返回updated=true - 继续循环检查新的堆顶
- 状态检查:
-
重复直到堆顶稳定
- 堆顶无需调整时退出
操作后状态:
graph TD
A["[0] 15ns<br/>Normal"] --> B["[1] 20ns<br/>Normal"]
A --> C["[2] 25ns<br/>Normal"]
A --> D["[3] 40ns<br/>Normal"]
A --> E["[4] 35ns<br/>Normal"]
style A fill:#4caf50,color:#fff
style B fill:#e8f5e8
style C fill:#e8f5e8
style D fill:#e8f5e8
style E fill:#e8f5e8
3.7.8 updateHeap - 更新单个定时器在堆中的状态
updateHeap源码分析:
// updateHeap 根据 t.state 更新 t,更新 t.state
// 并返回一个布尔值,指示状态(以及 ts.heap[0].when)是否已更改。
func (t *timer) updateHeap() (updated bool) {
assertWorldStoppedOrLockHeld(&t.mu)
t.trace("updateHeap")
ts := t.ts
if ts == nil || t != ts.heap[0].timer {
badTimer() // 定时器必须是堆顶
}
assertLockHeld(&ts.mu)
// 情况1: 处理僵尸定时器(已停止但仍在堆中)
if t.state&timerZombie != 0 {
// 从堆中移除定时器
t.state &^= timerHeaped | timerZombie | timerModified
ts.zombies.Add(-1)
ts.deleteMin()
return true
}
// 情况2: 处理已修改的定时器
if t.state&timerModified != 0 {
// 更新堆中的触发时间并重新调整位置
t.state &^= timerModified
ts.heap[0].when = t.when
ts.siftDown(0)
ts.updateMinWhenHeap()
return true
}
// 情况3: 无需更新
return false
}
updateHeap操作示例:
flowchart TD
Start["开始 updateHeap()"] --> CheckZombie{"检查 timerZombie?"}
CheckZombie --> |"是"| CleanZombie["清理僵尸定时器<br/>state &^= flags"]
CheckZombie --> |"否"| CheckModified{"检查 timerModified?"}
CleanZombie --> DecZombie["减少计数<br/>zombies.Add(-1)"]
DecZombie --> DeleteMin["调用 deleteMin()<br/>移除堆顶"]
DeleteMin --> ReturnTrue1["返回 updated=true"]
CheckModified --> |"是"| UpdateTime["更新触发时间<br/>when = nextwhen"]
CheckModified --> |"否"| ReturnFalse["返回 updated=false"]
UpdateTime --> ClearFlag["清除修改标志<br/>state &^= timerModified"]
ClearFlag --> Adjust["调用 adjust()<br/>重新调整堆位置"]
Adjust --> ReturnTrue2["返回 updated=true"]
style Start fill:#e3f2fd
style CleanZombie fill:#ffebee
style UpdateTime fill:#fff3e0
style ReturnTrue1 fill:#4caf50,color:#fff
style ReturnTrue2 fill:#4caf50,color:#fff
style ReturnFalse fill:#9e9e9e,color:#fff
3.7.9 adjust - 批量调整堆中的修改定时器
操作前状态模拟:
graph TD
A["[0] 10ns<br/>Normal"] --> B["[1] 20ns<br/>Modified"]
A --> C["[2] 15ns<br/>Normal"]
A --> D["[3] 40ns<br/>Modified"]
A --> E["[4] 25ns<br/>Modified"]
B --> F["[5] 35ns<br/>Zombie"]
B --> G["[6] 50ns<br/>Normal"]
B --> H["[7] 30ns<br/>Normal"]
style A fill:#e8f5e8
style B fill:#fff3e0
style C fill:#e8f5e8
style D fill:#fff3e0
style E fill:#fff3e0
style F fill:#ffebee
style G fill:#e8f5e8
style H fill:#e8f5e8
状态说明:
- 🟢 Normal: 无需处理
- 🟡 Modified: 需要更新时间
- 🔴 Zombie: 需要删除
adjust源码分析:
// adjust looks through the timers in ts.heap for
// any timers that have been modified to run earlier, and puts them in
// the correct place in the heap.
// adjust 检查 ts.heap 中的定时器,查找任何已修改为更早运行的定时器,
// 并将它们放在堆中的正确位置。
func (ts *timers) adjust(now int64, force bool) {
ts.trace("adjust")
assertLockHeld(&ts.mu)
// 步骤1: 检查是否需要立即调整
if !force {
first := ts.minWhenModified.Load()
if first == 0 || first > now {
if verifyTimers {
ts.verify()
}
return // 最早的修改定时器还未到期
}
}
// 步骤2: 保护 wakeTime 的正确性
// 详细解释见源码注释...
ts.minWhenHeap.Store(ts.wakeTime())
ts.minWhenModified.Store(0)
changed := false
// 步骤3: 遍历所有定时器
for i := 0; i < len(ts.heap); i++ {
tw := &ts.heap[i]
t := tw.timer
if t.ts != ts {
throw("bad ts")
}
// 步骤4: 快速检查是否需要处理
if t.astate.Load()&(timerModified|timerZombie) == 0 {
continue // 无需调整
}
// 步骤5: 锁定定时器并处理
t.lock()
switch {
case t.state&timerHeaped == 0:
badTimer() // 定时器应该在堆中
case t.state&timerZombie != 0:
// 处理僵尸定时器
ts.zombies.Add(-1)
t.state &^= timerHeaped | timerZombie | timerModified
n := len(ts.heap)
ts.heap[i] = ts.heap[n-1]
ts.heap[n-1] = timerWhen{}
ts.heap = ts.heap[:n-1]
t.ts = nil
i-- // 重新检查当前位置
changed = true
case t.state&timerModified != 0:
// 处理修改的定时器
tw.when = t.when
t.state &^= timerModified
changed = true
}
t.unlock()
}
// 步骤6: 如果有变化,重建堆结构
if changed {
ts.initHeap()
}
// 步骤7: 更新最小堆时间
ts.updateMinWhenHeap()
if verifyTimers {
ts.verify()
}
}
adjust操作流程图:
flowchart TD
Start["开始 adjust(now, force)"]
Start --> CheckForce{"force = true?"}
CheckForce --> |"否"| CheckTime["检查 minWhenModified"]
CheckTime --> TimeCheck{"first == 0 或 first > now?"}
TimeCheck --> |"是"| Return1["直接返回"]
CheckForce --> |"是"| Preserve["保护 wakeTime 正确性"]
TimeCheck --> |"否"| Preserve
Preserve --> Step1["minWhenHeap.Store(wakeTime())"]
Step1 --> Step2["minWhenModified.Store(0)"]
Step2 --> Loop["遍历 heap[i]"]
Loop --> CheckState{"astate & (Modified|Zombie)?"}
CheckState --> |"无标志"| Continue["继续下一个"]
CheckState --> |"有标志"| Lock["t.lock()"]
Lock --> StateCheck{"检查 t.state"}
StateCheck --> |"timerZombie"| Zombie["删除僵尸定时器"]
StateCheck --> |"timerModified"| Modified["更新修改定时器"]
StateCheck --> |"其他"| BadState["badTimer()"]
Zombie --> RemoveZ["从堆中删除<br/>i--,changed=true"]
Modified --> UpdateM["tw.when = t.when<br/>changed=true"]
RemoveZ --> Unlock["t.unlock()"]
UpdateM --> Unlock
Unlock --> Continue
Continue --> LoopEnd{"遍历完成?"}
LoopEnd --> |"否"| Loop
LoopEnd --> |"是"| CheckChanged{"changed?"}
CheckChanged --> |"是"| InitHeap["initHeap()<br/>重建堆结构"]
CheckChanged --> |"否"| UpdateMin["updateMinWhenHeap()"]
InitHeap --> UpdateMin
UpdateMin --> End["结束"]
style Start fill:#e3f2fd
style CheckForce fill:#fff3e0
style Preserve fill:#e8f5e8
style Loop fill:#f3e5f5
style Zombie fill:#ffebee
style Modified fill:#e1f5fe
style InitHeap fill:#4caf50,color:#fff
style End fill:#2196f3,color:#fff
操作后状态:
graph TD
A["[0] 10ns<br/>Normal"] --> B["[1] 15ns<br/>Normal"]
A --> C["[2] 20ns<br/>Normal"]
A --> D["[3] 30ns<br/>Normal"]
A --> E["[4] 25ns<br/>Normal"]
B --> F["[5] 50ns<br/>Normal"]
B --> G["[6] 40ns<br/>Normal"]
style A fill:#4caf50,color:#fff
style B fill:#e8f5e8
style C fill:#e8f5e8
style D fill:#e8f5e8
style E fill:#e8f5e8
style F fill:#e8f5e8
style G fill:#e8f5e8
- 所有Modified定时器已更新时间
- 所有Zombie定时器已被删除
- 堆重新调整保持最小堆性质
- 堆大小从8减少到7
3.7.10 check - 检查并运行到期定时器
check源码分析:
// check runs any timers in ts that are ready.
// check 运行 ts 中准备就绪的任何定时器。
func (ts *timers) check(now int64) (rnow, pollUntil int64, ran bool) {
ts.trace("check")
// 步骤1: 获取下一个需要处理的时间点
next := ts.wakeTime()
if next == 0 {
return now, 0, false // 没有定时器
}
if now == 0 {
now = nanotime()
}
// 步骤2: 检查是否需要强制清理僵尸定时器
zombies := ts.zombies.Load()
if zombies < 0 {
badTimer()
}
force := ts == &getg().m.p.ptr().timers && int(zombies) > int(ts.len.Load())/4
// 步骤3: 提前返回检查
if now < next && !force {
return now, next, false // 还没到时间且无需强制清理
}
// 步骤4: 锁定并处理定时器
ts.lock()
if len(ts.heap) > 0 {
// 调整堆结构
ts.adjust(now, false)
// 运行所有到期的定时器
for len(ts.heap) > 0 {
if tw := ts.run(now); tw != 0 {
if tw > 0 {
pollUntil = tw
}
break
}
ran = true
}
// 延迟强制清理提升性能
force = ts == &getg().m.p.ptr().timers && int(ts.zombies.Load()) > int(ts.len.Load())/4
if force {
ts.adjust(now, true)
}
}
ts.unlock()
return now, pollUntil, ran
}
3.7.11 run - 运行单个定时器
操作前状态模拟:
graph TD
A["[0] 10ns<br/>即将触发"] --> B["[1] 20ns"]
A --> C["[2] 15ns"]
A --> D["[3] 40ns"]
A --> E["[4] 50ns"]
B --> F["[5] 25ns"]
B --> G["[6] 35ns"]
B --> H["[7] 50ns"]
B --> I["[8] 45ns"]
style A fill:#ff5722,color:#fff
style B fill:#f3e5f5
style C fill:#4caf50,color:#fff
style D fill:#f3e5f5
style E fill:#ffebee
style F fill:#fff3e0
style G fill:#fff3e0
style H fill:#fff3e0
style I fill:#fff3e0
当前状态:
- 堆顶定时器 when=10ns 已到期
- 需要执行回调函数
- 执行后需要重新调整堆结构
当前堆状态:
索引: [0, 1, 2, 3, 4]
when: [5, 8, 10, 15, 12]
now = 7ns (当前时间)
堆顶定时器: when=5ns < now=7ns (已到期)
run源码分析:
// run 检查 ts 中的第一个定时器。如果根据 now 它已准备就绪,
// 它运行定时器并删除或更新它。
//go:systemstack
func (ts *timers) run(now int64) int64 {
ts.trace("run")
assertLockHeld(&ts.mu)
Redo:
// 步骤1: 检查堆是否为空
if len(ts.heap) == 0 {
return -1
}
// 步骤2: 获取堆顶定时器
tw := ts.heap[0]
t := tw.timer
if t.ts != ts {
throw("bad ts")
}
// 步骤3: 快速路径检查
if t.astate.Load()&(timerModified|timerZombie) == 0 && tw.when > now {
return tw.when // 尚未到期
}
// 步骤4: 锁定定时器并更新状态
t.lock()
if t.updateHeap() {
t.unlock()
goto Redo // 堆结构已改变,重新开始
}
// 步骤5: 验证定时器状态
if t.state&timerHeaped == 0 || t.state&timerModified != 0 {
badTimer()
}
// 步骤6: 检查是否到期
if t.when > now {
t.unlock()
return t.when // 尚未到期
}
// 步骤7: 运行定时器
t.unlockAndRun(now)
assertLockHeld(&ts.mu) // t已解锁,但ts仍锁定
return 0 // 成功运行定时器
}
run操作流程图:
flowchart TD
Start["开始 run()"] --> CheckEmpty{"堆是否为空?"}
CheckEmpty --> |"是"| Return1["返回 0"]
CheckEmpty --> |"否"| GetFirst["获取堆顶定时器"]
GetFirst --> CheckTime{"是否到期?<br/>now >= when"}
CheckTime --> |"否"| Return2["返回 when-now"]
CheckTime --> |"是"| LockTimer["锁定定时器"]
LockTimer --> CheckState{"检查定时器状态"}
CheckState --> |"已删除"| Continue1["继续下一个"]
CheckState --> |"正常"| ExecuteCallback["执行回调函数"]
ExecuteCallback --> CheckPeriod{"是否周期性?"}
CheckPeriod --> |"是"| UpdatePeriod["更新下次触发时间<br/>when += period"]
CheckPeriod --> |"否"| RemoveTimer["从堆中删除"]
UpdatePeriod --> SiftDown1["siftDown调整"]
RemoveTimer --> DeleteMin["deleteMin删除"]
SiftDown1 --> CheckEmpty
DeleteMin --> CheckEmpty
Continue1 --> CheckEmpty
style Start fill:#e3f2fd
style ExecuteCallback fill:#4caf50,color:#fff
style UpdatePeriod fill:#fff3e0
style RemoveTimer fill:#ffebee
操作后状态:
graph TD
A["[0] 15ns<br/>新的堆顶"] --> B["[1] 20ns"]
A --> C["[2] 35ns"]
A --> D["[3] 40ns"]
A --> E["[4] 50ns"]
B --> F["[5] 25ns"]
B --> G["[6] 50ns"]
B --> H["[7] 45ns"]
style A fill:#4caf50,color:#fff
style B fill:#f3e5f5
style C fill:#fff3e0
style D fill:#f3e5f5
style E fill:#ffebee
style F fill:#fff3e0
style G fill:#fff3e0
style H fill:#fff3e0
执行后状态:
- 原堆顶定时器(10ns)已执行并删除
- 新堆顶变为15ns
- 堆大小从9减少到8
- 保持最小堆性质
- 下次唤醒时间为15ns
3.7.12 unlockAndRun - 定时器执行核心逻辑
unlockAndRun是定时器系统的核心执行函数,负责在定时器触发时安全地执行回调函数。这个函数处理了复杂的并发同步问题,确保定时器的正确执行。
函数签名和基本逻辑:
// unlockAndRun 解锁定时器并运行其函数。
// 调用者必须持有 t.mu 和 t.ts.mu(如果 t.ts != nil)。
func (t *timer) unlockAndRun(now int64) {
t.trace("unlockAndRun")
assertLockHeld(&t.mu)
if t.ts != nil {
assertLockHeld(&t.ts.mu)
}
// 竞争检测支持
if raceenabled {
// 注意我们在系统栈上运行,
// 所以在此函数执行时没有机会从我们下面重新分配 getg().m。
tsLocal := &getg().m.p.ptr().timers
if tsLocal.raceCtx == 0 {
tsLocal.raceCtx = racegostart(abi.FuncPCABIInternal((*timers).run) + sys.PCQuantum)
}
raceacquirectx(tsLocal.raceCtx, unsafe.Pointer(t))
}
// 检查定时器状态的合法性
if t.state&(timerModified|timerZombie) != 0 {
badTimer()
}
// 保存执行参数
f := t.f
arg := t.arg
seq := t.seq
var next int64
delay := now - t.when
// 处理周期性定时器
if t.period > 0 {
// 留在堆中但调整下次触发时间
next = t.when + t.period*(1+delay/t.period)
if next < 0 { // 检查溢出
next = maxWhen
}
} else {
next = 0 // 一次性定时器
}
// 更新定时器状态
ts := t.ts
t.when = next
if t.state&timerHeaped != 0 {
t.state |= timerModified
if next == 0 {
t.state |= timerZombie
t.ts.zombies.Add(1)
}
t.updateHeap()
}
// 处理通道定时器的并发控制
async := debug.asynctimerchan.Load() != 0
if !async && t.isChan && t.period == 0 {
// 告诉 Stop/Reset 我们正在发送一个值
if t.isSending.Add(1) < 0 {
throw("too many concurrent timer firings")
}
}
// 解锁定时器
t.unlock()
// 设置竞争检测上下文
if raceenabled {
// 临时为 g0 使用当前 P 的 racectx
gp := getg()
if gp.racectx != 0 {
throw("unexpected racectx")
}
gp.racectx = gp.m.p.ptr().timers.raceCtx
}
// 解锁定时器集合
if ts != nil {
ts.unlock()
}
// 通道定时器的序列号检查机制
if !async && t.isChan {
// 对于定时器通道,我们想确保在 t.stop 或 t.modify 之后
// 不会发生过时的发送,但由于锁顺序,我们不能在实际发送期间
//(f 做的)持有 t.mu。
//
// 序列号机制工作原理:
// 1. t.stop 和 t.modify 都在持有 t.mu 和 t.sendLock 时递增 t.seq
// 2. 我们在持有 t.mu 时复制了上面的 seq 值
// 3. 现在获取 t.sendLock 并检查 t.seq 是否仍然匹配
// 4. 如果不匹配,说明定时器已被更新,跳过发送
lock(&t.sendLock)
if t.period == 0 {
// 我们承诺可能基于 seq 发送一个值,
// 所以不需要继续告诉 stop/modify 我们正在发送
if t.isSending.Add(-1) < 0 {
throw("mismatched isSending updates")
}
}
// 序列号检查:如果不匹配则跳过发送
if t.seq != seq {
f = func(any, uintptr, int64) {}
}
}
// 执行定时器回调函数
f(arg, seq, delay)
// 清理工作
if !async && t.isChan {
unlock(&t.sendLock)
}
if ts != nil {
ts.lock()
}
if raceenabled {
gp := getg()
gp.racectx = 0
}
}
关键设计要点分析:
1. 锁顺序和并发安全
// 函数开始时的锁状态:
// - 持有 t.mu (定时器锁)
// - 持有 t.ts.mu (定时器集合锁)
//
// 执行过程中的锁操作:
// 1. 先解锁 t.mu
// 2. 再解锁 t.ts.mu
// 3. 对于通道定时器,获取 t.sendLock
// 4. 执行回调函数
// 5. 释放 t.sendLock
// 6. 重新获取 t.ts.mu
2. 周期性定时器处理
if t.period > 0 {
// 计算下次触发时间,考虑延迟补偿
next = t.when + t.period*(1+delay/t.period)
if next < 0 { // 防止整数溢出
next = maxWhen
}
}
3. 序列号机制防止竞态条件
序列号机制解决了以下竞态条件问题:
sequenceDiagram
participant T as Timer
participant U as unlockAndRun
participant S as Stop/Reset
Note over T: seq = 5
U->>T: 保存 seq = 5
U->>T: 解锁 t.mu
S->>T: 获取 t.mu + t.sendLock
S->>T: 递增 seq = 6
S->>T: 解锁
U->>T: 获取 t.sendLock
U->>T: 检查 seq != 5
U->>U: 跳过发送 (f = noop)
U->>T: 执行 f (无操作)
4. isSending字段的作用
isSending字段用于协调定时器执行和停止/重置操作:
- 递增时机:准备发送通道值之前
- 递减时机:确定要发送后(基于序列号检查)
- 检查时机:
stop/reset操作检查是否有正在进行的发送
5. 延迟补偿机制
delay := now - t.when // 计算实际延迟
f(arg, seq, delay) // 将延迟传递给回调函数
对于通道定时器,time包会使用这个延迟值来调整发送的时间戳,使其看起来像是准时发送的。
3.7.13 性能分析总结
各操作的时间复杂度:
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
| addHeap | O(log₄ N) | 向上调整,四叉堆高度约为 log₄ N |
| deleteMin | O(log₄ N) | 向下调整 |
| siftUp | O(log₄ N) | 最多比较 log₄ N 层 |
| siftDown | O(log₄ N) | 每层最多比较3次(4个子节点) |
| updateHeap | O(log₄ N) | 可能触发siftDown |
| cleanHead | O(K) | K为需要清理的定时器数量 |
| adjust | O(N + K×log₄ N) | N次检查 + K次堆调整 |
| check | O(K×log₄ N) | 运行K个到期定时器 |
| run | O(log₄ N) | 单个定时器处理 |
四叉堆优势验证:
graph TD
subgraph "四叉堆结构特点"
A["每个节点最多4个子节点"]
B["父节点 ≤ 所有子节点"]
C["树高度: log₄(N)"]
D["缓存友好的内存布局"]
end
subgraph "索引计算公式"
E["父节点: (i-1)/4"]
F["第1个子节点: 4*i+1"]
G["第2个子节点: 4*i+2"]
H["第3个子节点: 4*i+3"]
I["第4个子节点: 4*i+4"]
end
subgraph "性能优势"
J["比较次数减少50%"]
K["树高度减少50%"]
L["缓存命中率提升"]
M["内存访问更连续"]
end
A --> E
B --> F
C --> J
D --> L
style A fill:#e1f5fe
style J fill:#4caf50,color:#fff
style K fill:#4caf50,color:#fff
style L fill:#4caf50,color:#fff
性能对比分析 (1000个定时器):
| 堆类型 | 树高度 | siftUp比较 | siftDown比较 | 内存位置 | 缓存效率 |
|---|---|---|---|---|---|
| 二叉堆 | ~10层 | 最多10次 | 最多20次 | 10个不同位置 | 一般 |
| 四叉堆 | ~5层 | 最多5次 | 最多15次 | 5个不同位置 | 优秀 |
| 提升 | 50%↓ | 50%↓ | 25%↓ | 50%↓ | 显著提升 |
这种精心设计的四叉堆结构是Go定时器系统高性能的关键基础!
3.8 定时器调度关键点
flowchart TD
Start["调度器寻找工作"] --> Check1["关键点1: 本地定时器检查"]
Check1 --> Local["findRunnable()<br/>检查当前P的定时器"]
Local --> LocalTimer["pp.timers.check(now)"]
LocalTimer --> LocalResult{"有定时器触发?"}
LocalResult --> |"是"| Restart1["重新检查运行队列"]
LocalResult --> |"否"| Check2["关键点2: stealWork检查"]
Check2 --> Steal["遍历所有其他P"]
Steal --> StealGoro["尝试偷取goroutine"]
StealGoro --> StealResult{"偷取成功?"}
StealResult --> |"是"| RunGoro["运行偷取的goroutine"]
StealResult --> |"否"| StealTimer["检查其他P的定时器"]
StealTimer --> StealTimerResult{"有到期定时器?"}
StealTimerResult --> |"是"| RunOtherTimer["runOneTimer()"]
StealTimerResult --> |"否"| Check3["关键点3: checkTimersNoP"]
Check3 --> FinalCheck["最终全局定时器检查"]
FinalCheck --> AllP["遍历所有P的定时器"]
AllP --> FinalResult{"有到期定时器?"}
FinalResult --> |"是"| RunFinal["运行全局定时器"]
FinalResult --> |"否"| Sleep["进入休眠等待"]
RunOtherTimer --> Restart2["标记有新工作"]
RunFinal --> Restart3["更新pollUntil"]
Restart1 --> Start
Restart2 --> Start
Restart3 --> Start
style Check1 fill:#e3f2fd
style Check2 fill:#fff3e0
style Check3 fill:#ffebee
style RunGoro fill:#4caf50,color:#fff
style RunOtherTimer fill:#4caf50,color:#fff
style RunFinal fill:#4caf50,color:#fff
style Sleep fill:#9e9e9e,color:#fff
3.8.1 关键点1:本地定时器检查
在findRunnable函数中,当本地运行队列为空时,会首先检查当前P的定时器:
// 检查本地定时器
if runqempty(pp) {
now, pollUntil, ran := pp.timers.check(nanotime())
if ran {
// 有定时器触发,重新检查运行队列
goto top
}
}
主要逻辑:
- 触发条件:当前P的本地运行队列为空时
- 检查范围:仅检查当前P的定时器堆
- 核心操作:调用
pp.timers.check(nanotime()) - 处理流程:
- 获取当前时间和下一个唤醒时间
- 检查是否需要强制清理僵尸定时器
- 如果有定时器到期,锁定堆并运行所有到期定时器
- 调整堆结构,清理已处理的定时器
- 返回结果:如果有定时器运行,返回
ran=true,调度器会重新检查运行队列 - 性能特点:优先级最高,只处理本地定时器,开销最小
3.8.2 关键点2:stealWork中的定时器检查
// stealWork 尝试从任何P中偷取可运行的goroutine或定时器。
func stealWork(now int64) (gp *g, inheritTime bool, rnow, pollUntil int64, newWork bool) {
pp := getg().m.p.ptr()
ranTimer := false
// 遍历所有P,尝试偷取工作
enum := stealOrder.start(fastrand())
for enum.done() {
p2 := allp[enum.position()]
if pp == p2 {
continue // 跳过自己
}
// 尝试偷取goroutine
if gp := runqsteal(pp, p2, stealRunNextG); gp != nil {
return gp, false, now, pollUntil, ranTimer
}
// 如果没有偷到goroutine,尝试运行其他P的定时器
if !ranTimer {
w := p2.timers.wakeTime()
if w != 0 && (pollUntil == 0 || w < pollUntil) {
pollUntil = w
}
// 如果其他P的定时器已经到期,运行它们
if w != 0 && w <= now {
if runOneTimer(p2, now) {
ranTimer = true
newWork = true
}
}
}
enum.next()
}
return nil, false, now, pollUntil, ranTimer
}
主要逻辑:
- 触发条件:本地定时器检查无果,且无法偷取到其他P的goroutine时
- 检查范围:遍历所有其他P的定时器(跳过当前P)
- 核心操作:调用
runOneTimer(p2, now)运行其他P的单个定时器 - 处理流程:
- 随机顺序遍历所有P,避免总是从同一个P偷取
- 优先尝试偷取goroutine,失败后才检查定时器
- 获取其他P的下一个唤醒时间
wakeTime() - 如果定时器已到期,运行一个定时器后立即返回
- 更新全局的
pollUntil时间
- 限制机制:每次调用最多运行一个定时器(
ranTimer标志控制) - 性能特点:跨P操作,需要额外的锁开销,但能充分利用系统资源
3.8.3 关键点3:checkTimersNoP最终检查
// checkTimersNoP 运行P中准备就绪的任何定时器。
// 当P变为空闲时由调度器调用。
func checkTimersNoP(allpSnapshot []*p, timerpMaskSnapshot pMask, pollUntil int64) int64 {
// 遍历所有P的定时器
for id, pp := range allpSnapshot {
if pp == nil {
continue
}
if !timerpMaskSnapshot.read(uint32(id)) {
continue // 此P没有定时器
}
w := pp.timers.wakeTime()
if w == 0 {
continue // 没有活跃定时器
}
if pollUntil == 0 || w < pollUntil {
pollUntil = w
}
if w <= now {
// 定时器已到期,运行它
if runOneTimer(pp, now) {
// 有定时器运行,更新pollUntil
pollUntil = 0
}
}
}
return pollUntil
}
主要逻辑:
- 触发条件:所有P都没有可运行的goroutine,系统即将进入休眠前的最后检查
- 检查范围:全局检查所有P的定时器(包括当前P)
- 核心操作:使用快照机制安全地遍历所有P的定时器
- 处理流程:
- 使用
allpSnapshot和timerpMaskSnapshot避免并发修改 - 通过
timerpMaskSnapshot快速跳过没有定时器的P - 收集所有P的最早唤醒时间,更新全局
pollUntil - 运行所有已到期的定时器
- 如果有定时器运行,重置
pollUntil=0触发重新调度
- 使用
- 安全机制:使用快照避免在遍历过程中P被修改或销毁
- 性能特点:全局扫描,开销最大,但确保不遗漏任何到期定时器
与stealWork的关键区别:
| 特性 | stealWork定时器检查 | checkTimersNoP |
|---|---|---|
| 调用时机 | 偷取工作失败时 | 系统即将休眠前 |
| 检查范围 | 其他P(跳过当前P) | 所有P(包括当前P) |
| 处理数量 | 最多运行1个定时器 | 运行所有到期定时器 |
| 安全机制 | 直接访问P | 使用快照机制 |
| 性能开销 | 中等(随机遍历) | 最高(全局扫描) |
| 目标 | 寻找可执行工作 | 确保无遗漏 |
| 返回条件 | 找到工作立即返回 | 完整扫描后返回 |
3.9 三个关键点的调用时序
sequenceDiagram
participant S as 调度器
participant P1 as 当前P
participant P2 as 其他P
participant NP as 网络轮询器
S->>P1: 1. 检查本地运行队列
P1-->>S: 队列为空
S->>P1: 2. 关键点1: 本地定时器检查
P1->>P1: timers.check(now)
P1-->>S: 有定时器运行?
alt 有定时器运行
S->>S: goto top (重新开始)
else 无定时器运行
S->>P2: 3. 关键点2: stealWork
P2->>P2: 尝试偷取goroutine
P2-->>S: 偷取失败
P2->>P2: runOneTimer(p2, now)
P2-->>S: 运行了定时器?
alt 运行了定时器
S->>S: 标记newWork=true
else 无定时器运行
S->>S: 4. 关键点3: checkTimersNoP
S->>S: 遍历所有P的定时器
S->>S: 运行所有到期定时器
S-->>NP: 计算pollUntil
alt 有定时器运行
S->>S: 重新调度
else 无定时器运行
S->>NP: 进入网络轮询等待
end
end
end
3.10 完整的Sleep流程图
graph TD
A["用户调用 time.Sleep(d)"] --> B["timeSleep(ns)"]
B --> C["getg().timer<br/>获取/创建goroutine定时器"]
C --> D["计算唤醒时间<br/>when = nanotime() + ns"]
D --> E["设置 gp.sleepWhen = when"]
E --> F["gopark(resetForSleep, ...)<br/>挂起当前goroutine"]
F --> G["resetForSleep(gp, nil)"]
G --> H["gp.timer.reset(when, 0)"]
H --> I["timer.modify(when, 0, ...)"]
I --> J["设置 t.when = when"]
J --> K["needsAdd() 检查是否需要添加"]
K --> L["maybeAdd() 添加到定时器堆"]
L --> M["addHeap(t) 堆操作"]
M --> N["siftUp() 向上调整"]
N --> O["updateMinWhenHeap()"]
O --> P["wakeNetPoller() 通知轮询器"]
P --> Q["定时器在堆中等待..."]
Q --> R["调度器检查定时器<br/>findRunnable()"]
R --> S["timers.check(now)"]
S --> T["timers.run(now)"]
T --> U["timer.unlockAndRun(now)"]
U --> V["goroutineReady(gp)"]
V --> W["goready(gp)<br/>唤醒sleeping goroutine"]
W --> X["goroutine恢复执行"]
style A fill:#e3f2fd
style F fill:#ff9800,color:#fff
style L fill:#4caf50,color:#fff
style Q fill:#9e9e9e,color:#fff
style S fill:#2196f3,color:#fff
style W fill:#4caf50,color:#fff
style X fill:#e8f5e8
3.10 性能优化要点
- 定时器复用:每个goroutine复用timer,避免频繁分配
- 四叉堆结构:相比二叉堆,减少比较次数和树高度
- 延迟调整:定时器修改时标记而非立即调整堆
- 分布式设计:每个P独立的定时器堆,减少锁竞争
- 批量处理:adjust函数批量处理已修改的定时器
- 网络轮询集成:与epoll等机制集成,实现高效等待
这种设计使得Go语言的定时器系统在高并发场景下仍能保持优秀的性能表现。
第四部分:NewTimer机制源码深入分析
4.1 NewTimer与Sleep的核心差异
time.NewTimer()与time.Sleep()虽然都基于底层的timer结构,但在实现上有着本质的区别:
| 特性 | time.Sleep() | time.NewTimer() |
|---|---|---|
| 定时器类型 | 函数定时器 (isChan=false) | 通道定时器 (isChan=true) |
| 触发方式 | 直接唤醒goroutine | 向通道发送时间值 |
| 回调函数 | goroutineReady | sendTime |
| 同步机制 | gopark/goready | 通道通信 |
| 复用性 | goroutine级别复用 | 独立分配 |
| 并发安全 | 简单锁保护 | 复杂的序列号+双锁机制 |
4.2 NewTimer完整调用链分析
sequenceDiagram
participant U as 用户代码
participant TP as time包
participant RT as runtime
participant TS as timers堆
participant NP as netpoller
U->>TP: time.NewTimer(d)
TP->>TP: when(d) 计算触发时间
TP->>TP: make(chan Time, 1) 创建通道
TP->>RT: newTimer(when, 0, sendTime, c, syncTimer(c))
RT->>RT: new(timeTimer) 分配定时器
RT->>RT: t.timer.init(nil, nil) 初始化
RT->>RT: lockInit(&t.sendLock) 初始化发送锁
RT->>RT: t.isChan = true 标记为通道定时器
RT->>RT: c.timer = &t.timer 关联通道
RT->>RT: t.modify(when, 0, sendTime, c, 0)
Note over RT: modify函数处理通道定时器的复杂逻辑
RT->>RT: t.seq++ 递增序列号
RT->>RT: timerchandrain(c) 清空过时值
RT->>RT: t.maybeAdd() 添加到堆
RT->>TS: addHeap(t) 堆操作
TS->>NP: wakeNetPoller(when) 通知轮询器
RT-->>TP: 返回 *timeTimer
TP-->>U: 返回 *Timer
4.3 newTimer函数源码详解
newTimer是runtime中创建定时器的核心函数,它处理了通道定时器的所有复杂逻辑:
// newTimer 分配并返回具有给定参数的新 time.Timer 或 time.Ticker(相同布局)。
//go:linkname newTimer time.newTimer
func newTimer(when, period int64, f func(arg any, seq uintptr, delay int64), arg any, c *hchan) *timeTimer {
// 步骤1: 分配timeTimer结构体
// timeTimer包含用户可见的Timer字段和运行时内部状态
t := new(timeTimer)
// 步骤2: 初始化嵌入的timer结构
// 注意:这里传入nil, nil,回调函数和参数稍后在modify中设置
t.timer.init(nil, nil)
// 步骤3: 调试跟踪(如果启用)
t.trace("new")
// 步骤4: 竞争检测支持(如果启用)
if raceenabled {
racerelease(unsafe.Pointer(&t.timer))
}
// 步骤5: 通道定时器特殊处理
if c != nil {
// 5.1 初始化发送锁,用于保护通道发送操作
// lockRankTimerSend确保正确的锁排序,避免死锁
lockInit(&t.sendLock, lockRankTimerSend)
// 5.2 标记为通道定时器
// 这个标志影响整个定时器的处理逻辑
t.isChan = true
// 5.3 建立双向关联:通道 <-> 定时器
// 通道需要知道关联的定时器,用于Stop/Reset操作
c.timer = &t.timer
// 5.4 验证通道容量
// Go 1.23+要求通道容量>0,通常为1(缓冲通道)
if c.dataqsiz == 0 {
throw("invalid timer channel: no capacity")
}
}
// 步骤6: 设置定时器参数并添加到堆
// modify函数处理所有复杂的状态管理和堆操作
t.modify(when, period, f, arg, 0)
// 步骤7: 标记初始化完成
t.init = true
return t
}
4.4 通道定时器的modify逻辑差异
通道定时器在modify函数中有着与函数定时器完全不同的处理逻辑:
// modify函数中通道定时器的特殊处理(简化版)
func (t *timer) modify(when, period int64, f func(arg any, seq uintptr, delay int64), arg any, seq uintptr) bool {
// 检查是否为异步模式(Go 1.23+默认为false)
async := debug.asynctimerchan.Load() != 0
// === 通道定时器专用:获取发送锁 ===
if !async && t.isChan {
// 必须先获取sendLock,再获取t.mu
// 这个锁保护通道发送操作,防止竞态条件
lock(&t.sendLock)
}
// 获取定时器主锁
t.lock()
// 异步模式下的特殊处理(通常跳过)
if async {
t.maybeRunAsync()
}
// === 核心状态更新逻辑 ===
oldPeriod := t.period
t.period = period
if f != nil {
t.f = f // 设置为sendTime函数
t.arg = arg // 设置为通道指针
t.seq = seq // 序列号(通常为0)
}
// 计算是否需要唤醒网络轮询器
wake := false
pending := t.when > 0
t.when = when
// 如果定时器已在堆中,标记为已修改
if t.state&timerHeaped != 0 {
t.state |= timerModified
// ... 堆管理逻辑 ...
}
// 检查是否需要添加到堆
add := t.needsAdd()
// === 通道定时器专用:序列号机制 ===
if !async && t.isChan {
// 递增序列号,使所有正在进行的发送操作无效
// 这是防止过时值发送的关键机制
t.seq++
// 特殊情况:如果当前有发送正在进行
// 递增seq会阻止该发送,所以应该返回true(表示定时器被停止)
if oldPeriod == 0 && t.isSending.Load() > 0 {
pending = true
}
}
// 释放定时器主锁
t.unlock()
// === 通道定时器专用:清理过时值 ===
if !async && t.isChan {
// 清空通道中的过时时间值
// 这确保接收者不会收到来自之前定时器配置的值
if timerchandrain(t.hchan()) {
pending = true
}
// 释放发送锁
unlock(&t.sendLock)
}
// 如果需要,添加到定时器堆
if add {
t.maybeAdd()
}
// 如果需要,唤醒网络轮询器
if wake {
wakeNetPoller(when)
}
return pending
}
第五部分:After、AfterFunc、Ticker机制源码分析
虽然After、AfterFunc和Ticker在底层都使用相同的定时器基础设施,但它们在使用模式、生命周期管理和性能特征上有显著差异。本部分将深入分析这些差异。
5.1 三种定时器机制对比概览
| 特性维度 | time.After() | time.AfterFunc() | time.NewTicker() |
|---|---|---|---|
| 返回类型 | <-chan Time | *Timer | *Ticker |
| 触发次数 | 一次性 | 一次性 | 周期性重复 |
| 回调方式 | 通道发送 | 函数调用 | 通道发送 |
| goroutine创建 | 无 | 是(每次触发) | 无 |
| 资源管理 | 自动(Go 1.23+) | 手动Stop | 手动Stop |
| 适用场景 | 简单超时 | 异步回调 | 周期性任务 |
| 内存开销 | 最小 | 中等 | 最大 |
5.2 time.After源码分析
time.After是最简单的定时器API,本质上是NewTimer的便利包装:
// After 等待持续时间过去,然后在返回的通道上发送当前时间。
// 它等价于 [NewTimer](d).C。
//
// 在 Go 1.23 之前,此文档警告底层的 [Timer] 不会被垃圾收集器回收,
// 直到定时器触发,如果效率是个问题,代码应该使用 NewTimer 并在不再需要时调用 [Timer.Stop]。
// 从 Go 1.23 开始,垃圾收集器可以回收未引用的、未停止的定时器。
// 当 After 可以胜任时,没有理由首选 NewTimer。
func After(d Duration) <-chan Time {
return NewTimer(d).C
}
After的设计特点:
- 极简API:只返回通道,隐藏Timer对象
- 一次性使用:无法Stop或Reset
- GC友好:Go 1.23+自动回收
- 零配置:无需手动资源管理
使用模式对比:
// ✅ 推荐:简单超时场景
select {
case result := <-doWork():
// 工作完成
case <-time.After(5 * time.Second):
// 超时处理
}
// ❌ 不推荐:需要取消的场景
timer := time.NewTimer(5 * time.Second)
defer timer.Stop() // After无法提供这种控制
select {
case result := <-doWork():
// 工作完成,定时器被停止
case <-timer.C:
// 超时处理
}
5.3 time.AfterFunc源码分析
AfterFunc是唯一使用函数回调而非通道的定时器API:
// 它返回一个 [Timer],可以使用其 Stop 方法取消调用。
// 返回的 Timer 的 C 字段未使用,将为 nil。
func AfterFunc(d Duration, f func()) *Timer {
return (*Timer)(newTimer(when(d), 0, goFunc, f, nil))
}
// goFunc 是 AfterFunc 的回调包装器
// 它在新的 goroutine 中执行用户函数
func goFunc(arg any, seq uintptr, delta int64) {
go arg.(func())()
}
AfterFunc的关键差异:
- 回调函数:
goFunc而非sendTime - 无通道:
c参数为nil - goroutine创建:每次触发都创建新goroutine
- 异步执行:不阻塞定时器处理流程
goFunc实现分析:
// goFunc 的设计考虑:
// 1. 简单性:直接启动 goroutine,无复杂状态管理
// 2. 隔离性:用户函数在独立 goroutine 中运行,不影响定时器系统
// 3. 无阻塞:即使用户函数阻塞或panic,也不影响其他定时器
func goFunc(arg any, seq uintptr, delta int64) {
// arg 是用户提供的函数
// seq 和 delta 在函数定时器中未使用
go arg.(func())()
}
5.4 time.NewTicker源码分析
Ticker是唯一支持周期性触发的定时器:
// NewTicker 返回一个新的 [Ticker],包含一个通道,该通道将在每次滴答后发送当前时间。
// 滴答的周期由持续时间参数指定。Ticker 将调整时间间隔或丢弃滴答以补偿慢接收者。
// 持续时间 d 必须大于零;如果不是,NewTicker 将 panic。
func NewTicker(d Duration) *Ticker {
if d <= 0 {
panic("non-positive interval for NewTicker")
}
// 给通道一个 1 元素的时间缓冲区。
// 如果客户端在读取时落后,我们将滴答丢弃到地板上,直到客户端赶上。
c := make(chan Time, 1)
t := (*Ticker)(unsafe.Pointer(newTimer(when(d), int64(d), sendTime, c, syncTimer(c))))
t.C = c
return t
}
Ticker的关键特征:
- 周期性参数:
period = int64(d),而Timer的period为0 - 缓冲通道:容量为1,防止慢接收者阻塞
- 类型转换:通过
unsafe.Pointer在Timer和Ticker间转换 - 丢弃策略:慢接收者时丢弃滴答而非累积
5.5 周期性定时器的特殊处理
在runtime中,周期性定时器有特殊的重新调度逻辑:
// 在 timer.unlockAndRun 中的周期性处理逻辑
func (t *timer) unlockAndRun(now int64) {
// ... 前面的处理逻辑 ...
// 执行回调函数
f(arg, seq, delay)
// === 周期性定时器的关键处理 ===
t.lock()
if t.period > 0 {
// 计算下一次触发时间
// 基于原始when时间而非当前时间,避免累积误差
t.when += t.period
// 如果下一次触发时间已经过去,调整到合理的未来时间
if t.when < now {
// 计算应该跳过多少个周期
// 这处理了系统负载高或接收者慢的情况
t.when += (now - t.when + t.period - 1) / t.period * t.period
}
// 标记需要重新添加到堆中
t.state |= timerModified
// 重新添加到定时器堆
t.maybeAdd()
}
t.unlock()
}
周期性调度的设计要点:
- 基准时间保持:基于原始
when计算,避免时间漂移 - 跳跃处理:系统繁忙时跳过过期的周期
- 负载适应:自动调整以适应系统负载
5.6 缓冲策略对比
不同定时器类型使用不同的缓冲策略:
// NewTimer: 同步通道(Go 1.23+)
c := make(chan Time, 1) // 实际上是无缓冲的同步通道
// NewTicker: 缓冲通道
c := make(chan Time, 1) // 真正的1元素缓冲
// After: 继承NewTimer的策略
return NewTimer(d).C
// AfterFunc: 无通道
// 直接函数调用,无缓冲概念
缓冲策略的影响:
| 策略 | 优势 | 劣势 | 适用场景 |
|---|---|---|---|
| 无缓冲(同步) | 精确控制,无过时值 | 可能阻塞发送者 | 精确定时 |
| 1元素缓冲 | 容忍慢接收者 | 可能丢失滴答 | 周期性任务 |
| 函数回调 | 无阻塞,高性能 | goroutine开销 | 异步处理 |
5.7 最佳实践指南
5.7.1 选择合适的定时器类型
// ✅ 使用 After:简单超时
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// ✅ 使用 AfterFunc:异步清理
cleanupTimer := time.AfterFunc(30*time.Second, func() {
cleanupResources()
})
defer cleanupTimer.Stop()
// ✅ 使用 Ticker:周期性任务
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
doPeriodicWork()
case <-done:
return
}
}
5.7.2 避免常见陷阱
// ❌ 错误:在循环中使用After
for {
select {
case <-time.After(1 * time.Second): // 每次都创建新定时器
doWork()
}
}
// ✅ 正确:使用Ticker
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
doWork()
}
}
// ❌ 错误:忘记停止Ticker
func badExample() {
ticker := time.NewTicker(1 * time.Second)
// 忘记调用 ticker.Stop()
for i := 0; i < 10; i++ {
<-ticker.C
}
} // ticker继续运行,造成资源泄漏
// ✅ 正确:确保停止Ticker
func goodExample() {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop() // 确保清理
for i := 0; i < 10; i++ {
<-ticker.C
}
}
总结
通过对Go语言定时器机制的深入源码分析,我们全面探索了从基础的time.Sleep()到复杂的NewTimer、After、AfterFunc和Ticker等API的实现原理。本文的分析揭示了Go定时器系统的精妙设计和工程智慧。
核心架构洞察
统一的底层基础设施:
- 所有定时器都基于相同的
timer结构体和最小堆管理 - 运行时系统通过
timers数组实现高效的定时器调度 - 网络轮询器(
netpoller)与定时器系统的深度集成确保了高性能