当前位置: 首页 > news >正文

深入 Go 底层原理(十四):timer 的实现与高性能定时器

1. 引言

time 包中的 TimerTicker 是 Go 中实现定时和周期性任务的基础工具。time.Sleep 相对简单,但 TimerTicker 的背后,是一套由 runtime 管理的高性能、可扩展的定时器系统。

本文将深入 runtime 的定时器实现,了解它是如何用一个**最小堆(Min-Heap)**高效地管理成千上万个定时器的。

2. timer 的核心数据结构

每个通过 time.NewTimertime.NewTicker 创建的定时器,在 runtime 中都对应一个 timer 结构体:

// src/runtime/time.go
type timer struct {when   int64 // 定时器被唤醒的时间点 (纳秒)period int64 // 周期性定时器的时间间隔,对于 Timer 来说是 0f      func(interface{}, uintptr) // 定时器触发时要执行的回调函数arg    interface{} // 回调函数的参数seq    uintptr// 定时器在堆中的索引,用于高效地调整和删除heap_idx int
}

所有待处理的 timer 都会被存放在一个全局的定时器最小堆中。这个堆是根据 when 字段来组织的,即唤醒时间最早的 timer 会在堆顶

3. 早期实现 (Go 1.9 及之前)
  • 使用一个全局的、由互斥锁保护的最小堆。

  • runtime.sysmon 监控线程会定期检查堆顶的 timer

  • 缺点:所有对定时器的操作(添加、删除、重置)都需要获取全局锁,当定时器数量巨大时,锁竞争会成为性能瓶颈。

4. 现代实现 (Go 1.10 ~ Go 1.13) - 四叉堆优化

为了减少锁竞争,Go 1.10 引入了分片锁的思想。它将一个大的全局堆拆分成了 64 个小的、各自有锁的四叉堆(4-heap)

  • 当添加一个定时器时,会根据当前 goroutine 所在的 P (Processor) 的 ID,将其分配到对应的四叉堆中。

  • sysmon 会遍历这 64 个堆,找出最早需要唤醒的 timer

  • 优点:显著降低了锁冲突的概率。

5. 最新实现 (Go 1.14+) - P 私有定时器堆

为了追求极致性能,Go 1.14 再次重构了定时器系统,将定时器与 P 进行了更紧密的绑定。

  • 每个 P (Processor) 都有一个自己的、无锁的定时器最小堆

  • 当一个 goroutine 在某个 P 上创建一个 timer 时,这个 timer 会被直接加入到该 P 的私有堆中。此过程完全无锁

  • 每个 P 在其调度循环中,会自己检查其私有堆的堆顶 timer 是否到期。

  • 此外,还有一个专门的**网络轮询器(netpoller)**线程,它会在没有 P 运行时,负责检查和触发全局的、已到期的定时器。

工作流程:

  1. time.NewTimer 创建一个 timer,并将其加入当前 P 的私有堆。

  2. P 的调度器在执行 Goroutine 的间隙,会检查自己私有堆的堆顶 timer

  3. 如果 timer 到期,P 会将其移出堆,并将其回调函数 f 封装成一个新的 goroutine 去执行。

  4. 如果一个 P 长时间空闲,它会将其私有的 timer 批量转移到一个全局的、带锁的堆中,交由 netpoller 统一处理。

这种设计将绝大多数定时器操作本地化到了 P 上,实现了无锁化,极大地提升了高并发下定时器的性能和可扩展性。

6. 实践陷阱:time.After 与内存泄漏

time.After(d) 本质上是 time.NewTimer(d).C 的语法糖。如果在 for 循环中不加控制地使用 time.After,每次循环都会创建一个新的 Timer 对象。即使 <-time.After(d) 因为其他 case 满足而没有执行,这个 Timer 对象仍然存在于 runtime 的定时器堆中,直到其到期后才会被 GC。这会造成大量的、不必要的内存和计算资源浪费。

正确做法:在循环中使用 time.NewTimer,并通过 timer.Reset() 来复用同一个定时器。

// 错误示例
for {select {case <-ch:// ...case <-time.After(time.Second): // 每次循环都创建新 Timerreturn}
}// 正确示例
timer := time.NewTimer(time.Second)
defer timer.Stop()
for {select {case <-ch:// ...case <-timer.C:return}
}
http://www.lryc.cn/news/608101.html

相关文章:

  • 卡尔曼滤波轨迹跟踪算法与MATLAB实现
  • 关于Web前端安全防御XSS攻防的几点考虑
  • 【软考中级网络工程师】知识点之 VRRP
  • 智能学号抽取系统V5.6.4重磅发布
  • 【Docker】RK3576-Debian上使用Docker安装Ubuntu22.04+ROS2
  • 28Rsync免密传输与定时备份
  • 【学习笔记】MySQL技术内幕InnoDB存储引擎——第9章 性能调优
  • leetcode热题——组合
  • Android性能优化--16K对齐深入解析及适配指南
  • 【数据结构初阶】--排序(二)--直接选择排序,堆排序
  • AI Agent开发学习系列 - LangGraph(10): 带有循环的Looping Graph(练习解答)
  • JavaScript特殊集合WeakMap 的使用及场景介绍
  • 【昇腾推理PaddleOCR】生产级部署方式
  • 什么是AWS Region和AWS Availability Zones
  • php完整处理word中表单数据的方法
  • Word怎样转换为PDF
  • 使用AWS免费EC2自建RustDesk远程桌面连接服务
  • 【iOS】3GShare仿写
  • 市政污水厂变频器联网改造方案-profibus转ethernet ip网关(通俗版)
  • 疏老师-python训练营-Day33 MLP神经网络的训练
  • 详解Python标准库之命令行界面库
  • 【05】OpenCV C#——OpenCvSharp 图像基本操作---转灰度图、边缘提取、兴趣区域ROI,图像叠加
  • MyBatisPlus之CRUD接口(IService与BaseMapper)
  • 西门子 G120 变频器全解析:从认知到参数设置
  • 技巧|SwanLab记录ROC曲线攻略
  • LINUX82 shell脚本变量分类;系统变量;变量赋值;四则运算;shell
  • 系统性学习数据结构-第一讲-算法复杂度
  • MySQL 内置函数
  • ADB 查看 CPU 信息、查看内存信息、查看硬盘信息
  • 排序算法大全:从插入到快速排序