timewheel

package module
v0.0.0-...-4d907be Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 4, 2026 License: Apache-2.0 Imports: 7 Imported by: 0

README

⏱️ Adaptive Hybrid Timer Scheduler (AHTS)

Go Reference Go Report Card

自适应混合定时调度器 —— 一种面向高并发在线服务的高性能定时任务调度方案。

传统时间轮的核心矛盾是"一种数据结构试图满足所有时间尺度的需求"。AHTS 通过分层异构架构,让每种数据结构只做它最擅长的事情。

✨ 核心特性

  • O(1) 插入与取消 — 近期任务通过稀疏时间轮 + 反向索引实现常数时间操作
  • 真正的 Tickless — 无任务时零 CPU 空转,精确睡眠到下一个到期时间点
  • 漏 tick 补偿 — GC 停顿或系统调度延迟不会导致任务丢失
  • 分层异构架构 — 近期任务使用稀疏时间轮,远期任务使用跳表,各取最优
  • 惰性迁移 — 远期任务在接近到期时才迁移到近期轮,避免无效计算
  • 并发安全 — 通过 race detector 验证,所有操作线程安全
  • 周期任务支持 — 内置周期任务自动重新调度,每次执行创建独立实例避免并发问题
  • 零依赖 — 纯标准库实现,无第三方依赖

🏗️ 架构

┌──────────────────────────────────────────────────────────────┐
│               Adaptive Hybrid Timer Scheduler                │
│                                                              │
│   ┌──────────────────────┐    ┌──────────────────────┐       │
│   │   Near-term Region   │    │   Far-term Region    │       │
│   │   [0, threshold)     │    │   [threshold, +∞)    │       │
│   │                      │    │                      │       │
│   │  Sparse Timing Wheel │◄───│     Skip List        │       │
│   │  • HashMap<tick,      │惰性│  • Sorted by time    │       │
│   │    HashMap<id,task>> │迁移│  • O(log n) ops      │       │
│   │  • Reverse index     │    │  • taskMap for O(1)  │       │
│   │    taskID → tick     │    │    lookup by ID      │       │
│   │  • Min-heap for O(1) │    │                      │       │
│   │    next-tick lookup  │    │                      │       │
│   └──────────┬───────────┘    └──────────────────────┘       │
│              │                                               │
│              ▼                                               │
│   ┌───────────────────────────┐                              │
│   │   Tickless Wake Engine    │                              │
│   │   • Precise sleep         │                              │
│   │   • Zero idle CPU         │                              │
│   │   • Interruptible by new  │                              │
│   │     tasks via CAS + chan  │                              │
│   └───────────────────────────┘                              │
└──────────────────────────────────────────────────────────────┘
运作流程
                    ┌─────────────┐
                    │  插入任务     │
                    └──────┬──────┘
                           │
                    delay < threshold?
                     ┌─────┴─────┐
                     │ Yes       │ No
                     ▼           ▼
              ┌────────────┐ ┌──────────┐
              │ 稀疏时间轮   │ │  跳表     │
              │ O(1) 插入   │ │ O(logn)  │
              └─────┬──────┘ └────┬─────┘
                    │             │
                    │    惰性迁移   │
                    │◄────────────┘
                    │   (接近到期时)
                    ▼
              ┌────────────┐
              │ Tickless   │
              │ 精确唤醒    │──→ 执行到期任务
              └────────────┘

📦 安装

go get github.com/atlaschan0010/timewheel

要求: Go 1.22+

🚀 快速开始

package main

import (
    "fmt"
    "time"
    "github.com/atlaschan0010/timewheel"
)

func main() {
    // 创建调度器(使用默认配置)
    scheduler := timewheel.New(timewheel.DefaultConfig())

    // 启动调度器
    scheduler.Start()
    defer scheduler.Stop()

    // 调度一个延迟任务
    scheduler.Schedule(5*time.Second, func() {
        fmt.Println("Task executed!")
    })

    // 调度可取消的任务
    taskID := scheduler.Schedule(10*time.Second, func() {
        fmt.Println("This won't print")
    })
    scheduler.Cancel(taskID) // O(1) 取消

    // 调度周期任务
    tickerID := scheduler.SchedulePeriodic(1*time.Second, func() {
        fmt.Println("Periodic task!")
    })

    time.Sleep(5 * time.Second)
    scheduler.Cancel(tickerID) // 停止周期任务

    time.Sleep(1 * time.Second)
}

⚙️ 配置

cfg := timewheel.Config{
    TickInterval:       1 * time.Millisecond,  // 近期轮精度
    NearTermThreshold:  60 * time.Second,       // 近期/远期分界线
    MigrationBatchSize: 128,                    // 每次迁移的最大任务数
    LazyDeleteEnabled:  true,                   // 启用惰性删除
}
scheduler := timewheel.New(cfg)
参数 默认值 说明
TickInterval 1ms 近期轮的最小精度。影响任务触发的时间误差上界;低于 1ms 的配置会按 1ms 处理
NearTermThreshold 60s 近期/远期分界线。低于此值的任务进入稀疏时间轮,否则进入跳表
MigrationBatchSize 128 每次 tick 最多从远期结构迁移到近期轮的任务数,防止迁移风暴
LazyDeleteEnabled true 启用惰性删除,取消时标记并移除索引,底层节点在到期弹出时物理清理
调优建议
场景 TickInterval NearTermThreshold 说明
WebSocket 心跳 100ms 120s 心跳精度通常不需要 1ms
游戏技能 CD 1ms 60s 需要毫秒级精度
RPC 超时 1ms 30s 超时范围短,阈值可降低
延迟队列 1s 3600s 精度要求低,阈值可放大

📖 API

调度任务
// 延迟任务(自动生成 ID)
taskID := scheduler.Schedule(delay, func() { ... })

// 带指定 ID 的延迟任务(相同 ID 会替换旧任务)
taskID := scheduler.ScheduleWithID("my-task", delay, func() { ... })

// 周期任务
tickerID := scheduler.SchedulePeriodic(interval, func() { ... })

// 带指定 ID 的周期任务
tickerID := scheduler.SchedulePeriodicWithID("my-ticker", interval, func() { ... })
取消任务
// 取消任务(近期任务 O(1),远期任务 O(log n))
cancelled := scheduler.Cancel(taskID)
任务替换
// 相同 ID 调度会自动取消旧任务
scheduler.ScheduleWithID("heartbeat", 30*time.Second, handler1)
// ... 稍后 ...
scheduler.ScheduleWithID("heartbeat", 30*time.Second, handler2) // handler1 被取消
统计信息
stats := scheduler.Stats()
fmt.Printf("Executed:   %d\n", stats.TasksExecuted)
fmt.Printf("Cancelled:  %d\n", stats.TasksCancelled)
fmt.Printf("Migrated:   %d\n", stats.TasksMigrated)
fmt.Printf("Near-term:  %d\n", stats.NearTermTasks)
fmt.Printf("Far-term:   %d\n", stats.FarTermTasks)
生命周期
scheduler.Start() // 启动调度引擎(幂等,多次调用安全)
scheduler.Stop()  // 停止调度引擎,等待当前执行中的回调完成

📊 性能

测试环境: Apple M3 Max, Go 1.25, macOS

BenchmarkSchedule-16              2,184,120       552.2 ns/op     253 B/op    4 allocs/op
BenchmarkCancel-16                6,472,408       238.6 ns/op       0 B/op    0 allocs/op
BenchmarkScheduleAndCancel-16     2,344,303       520.6 ns/op     388 B/op    7 allocs/op
操作 吞吐量 延迟 内存分配
调度 ~180 万/秒 552 ns 253 B, 4 allocs
取消 ~420 万/秒 239 ns 零分配
调度+取消 ~190 万/秒 521 ns 388 B, 7 allocs
对比传统方案
操作 传统时间轮 层级时间轮 Min-Heap AHTS
插入 O(1) O(1) O(log n) O(1) 近期
取消 O(n) 链表遍历 O(n) 链表遍历 O(n) 扫描 O(1) 反向索引
到期触发 O(k) O(k) O(log n) O(k)
空间 O(时间跨度) O(层数×轮大小) O(n) O(n) 稀疏
长延时 ❌ 溢出/内存爆 ✅ 需层级迁移 ✅ ✅ 惰性迁移
CPU 空转 每 tick 检查 每 tick 检查 sleep 到堆顶 ✅ Tickless
漏 tick 处理 ❌ 任务丢失 ❌ 任务丢失 ✅ ✅ 批量补偿

🔧 内部实现细节

稀疏时间轮 (SparseWheel)
存储结构:
  buckets:   map[tick]map[taskID]*Task    // 正向:tick → 任务集合
  taskIndex: map[taskID]tick              // 反向:任务 → 所在 tick
  tickHeap:  MinHeap<tick>                // 最小堆:O(1) 获取最早 tick

Insert(tick, task):   O(1) — HashMap 插入 + 堆 push
Delete(taskID):       O(1) — 反向索引定位 tick → HashMap 删除
PopTicksUpTo(max):    O(k) — 从堆顶连续弹出,k 为到期任务数
PeekNextTick():       O(1) — 堆顶元素
跳表 (SkipList)
存储结构:
  sorted nodes:  按 ExpireAt 排序的跳表节点
  taskMap:       map[taskID]*SkipListNode   // O(1) 按 ID 定位节点

Insert(task):        O(log n)
DeleteByID(taskID):  O(log n) — 先 O(1) 定位节点,再 O(log n) 更新指针
PeekMin():           O(1)    — 头节点的下一个
PopMin():            O(1) 摊还
Tickless 引擎 (TicklessEngine)
主循环:
  1. nextWake = min(nearTerm.PeekNextTick, farTerm.PeekMin)
  2. if nextWake == 0: 阻塞等待新任务(WaitForTask)
  3. SleepUntil(nextWake) — 可被新任务 CAS + channel 打断
  4. advance() — PopTicksUpTo(currentTick) + 惰性迁移

CAS 唤醒机制:
  NotifyIfEarlier(expireAt):
    for {
      current = atomicLoad()
      if expireAt < current:
        if CAS(current, expireAt): Wake(); return
        continue  // CAS 失败,重试(解决 ABA 问题)
      return
    }

🎯 适用场景

✅ 最佳匹配
场景 典型任务 为什么合适
WebSocket 网关 心跳超时、消息重发、空闲清理 大量定时器 + 频繁取消(断连)
游戏服务器 技能 CD、Buff、房间超时 毫秒精度 + 批量取消(玩家死亡)
微服务中间件 RPC 超时、熔断器、限流窗口 QPS 正比的定时器创建/取消
连接池/缓存 连接回收、缓存过期 混合延时 + 周期检查
⚠️ 非最佳场景
场景 推荐替代 原因
Cron 定时任务 (< 1K) time.AfterFunc / Min-Heap 任务量太少,O(1) vs O(log n) 无区别
数十亿级分布式定时 Redis / Temporal 单机内存不够
硬实时系统 固定数组时间轮 + RTOS 需要最坏 O(1),不能有 GC

🧪 测试

# 运行所有测试
go test -v ./...

# 运行 race detector
go test -race ./...

# 运行 benchmark
go test -bench=. -benchmem

📄 许可证

Apache-2.0 license

Documentation

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Config

type Config struct {
	TickInterval       time.Duration // 近期轮精度,默认 1ms
	NearTermThreshold  time.Duration // 近期/远期分界线,默认 60s
	MigrationBatchSize int           // 每次迁移的最大任务数,默认 128
	LazyDeleteEnabled  bool          // 是否启用惰性删除,默认 true
}

Config 调度器配置

func DefaultConfig

func DefaultConfig() Config

DefaultConfig 返回默认配置

type Scheduler

type Scheduler struct {
	// contains filtered or unexported fields
}

Scheduler 自适应混合定时调度器

Example
package main

import (
	"fmt"
	"time"

	"github.com/atlaschan0010/timewheel"
)

func main() {
	// 创建调度器
	cfg := timewheel.DefaultConfig()
	cfg.TickInterval = 10 * time.Millisecond // 加快示例速度
	scheduler := timewheel.New(cfg)

	// 启动调度器
	scheduler.Start()
	defer scheduler.Stop()

	// 示例 1: 延迟任务
	done := make(chan struct{})
	scheduler.Schedule(100*time.Millisecond, func() {
		fmt.Println("Delayed task executed!")
		close(done)
	})

	<-done

}
Output:
Delayed task executed!
Example (Cancel)
package main

import (
	"fmt"
	"time"

	"github.com/atlaschan0010/timewheel"
)

func main() {
	cfg := timewheel.DefaultConfig()
	cfg.TickInterval = 10 * time.Millisecond
	scheduler := timewheel.New(cfg)
	scheduler.Start()
	defer scheduler.Stop()

	executed := false
	taskID := scheduler.Schedule(100*time.Millisecond, func() {
		executed = true
		fmt.Println("This should not print!")
	})

	// 取消任务
	if scheduler.Cancel(taskID) {
		fmt.Println("Task cancelled successfully")
	}

	time.Sleep(200 * time.Millisecond)

	if !executed {
		fmt.Println("Task was not executed (as expected)")
	}

}
Output:
Task cancelled successfully
Task was not executed (as expected)
Example (Periodic)
package main

import (
	"fmt"
	"sync/atomic"
	"time"

	"github.com/atlaschan0010/timewheel"
)

func main() {
	cfg := timewheel.DefaultConfig()
	cfg.TickInterval = 10 * time.Millisecond
	scheduler := timewheel.New(cfg)
	scheduler.Start()
	defer scheduler.Stop()

	var count atomic.Int32
	tickerID := scheduler.SchedulePeriodic(50*time.Millisecond, func() {
		c := count.Add(1)
		fmt.Printf("Tick %d\n", c)
	})

	time.Sleep(180 * time.Millisecond)
	scheduler.Cancel(tickerID)

	fmt.Println("Ticker stopped")

}
Output:
Tick 1
Tick 2
Tick 3
Ticker stopped
Example (Stats)
package main

import (
	"fmt"
	"time"

	"github.com/atlaschan0010/timewheel"
)

func main() {
	cfg := timewheel.DefaultConfig()
	cfg.TickInterval = 10 * time.Millisecond
	scheduler := timewheel.New(cfg)
	scheduler.Start()
	defer scheduler.Stop()

	// 调度多个任务
	for i := 0; i < 5; i++ {
		scheduler.Schedule(50*time.Millisecond, func() {})
	}

	// 调度一个远期任务(超过 60s 阈值,默认在远期结构)
	scheduler.Schedule(70*time.Second, func() {})

	time.Sleep(150 * time.Millisecond)

	stats := scheduler.Stats()
	fmt.Printf("Executed: %d\n", stats.TasksExecuted)
	fmt.Printf("Near-term tasks: %d\n", stats.NearTermTasks)
	fmt.Printf("Far-term tasks: %d\n", stats.FarTermTasks)

}
Output:
Executed: 5
Near-term tasks: 0
Far-term tasks: 1
Example (WithID)
package main

import (
	"fmt"
	"time"

	"github.com/atlaschan0010/timewheel"
)

func main() {
	cfg := timewheel.DefaultConfig()
	cfg.TickInterval = 10 * time.Millisecond
	scheduler := timewheel.New(cfg)
	scheduler.Start()
	defer scheduler.Stop()

	done := make(chan struct{})

	// 使用自定义 ID
	scheduler.ScheduleWithID("my-task-1", 200*time.Millisecond, func() {
		fmt.Println("This should NOT print (replaced)")
	})

	// 更新同名任务(会替换旧任务)
	scheduler.ScheduleWithID("my-task-1", 100*time.Millisecond, func() {
		fmt.Println("Task 1 (updated) executed!")
		close(done)
	})

	<-done

}
Output:
Task 1 (updated) executed!

func New

func New(cfg Config) *Scheduler

New 创建新的调度器

func (*Scheduler) Cancel

func (s *Scheduler) Cancel(taskID string) bool

Cancel 取消任务

func (*Scheduler) Schedule

func (s *Scheduler) Schedule(delay time.Duration, fn func()) string

Schedule 调度一个延迟任务

func (*Scheduler) SchedulePeriodic

func (s *Scheduler) SchedulePeriodic(interval time.Duration, fn func()) string

SchedulePeriodic 调度周期任务

func (*Scheduler) SchedulePeriodicWithID

func (s *Scheduler) SchedulePeriodicWithID(id string, interval time.Duration, fn func()) string

SchedulePeriodicWithID 使用指定 ID 调度周期任务

func (*Scheduler) ScheduleWithID

func (s *Scheduler) ScheduleWithID(id string, delay time.Duration, fn func()) string

ScheduleWithID 使用指定 ID 调度延迟任务 如果 ID 已存在,旧任务会被取消并替换为新任务

func (*Scheduler) Start

func (s *Scheduler) Start()

Start 启动调度器

func (*Scheduler) Stats

func (s *Scheduler) Stats() Stats

Stats 返回统计信息

func (*Scheduler) Stop

func (s *Scheduler) Stop()

Stop 停止调度器

type SkipList

type SkipList struct {
	// contains filtered or unexported fields
}

SkipList 跳表实现(按 ExpireAt 排序)

func NewSkipList

func NewSkipList() *SkipList

NewSkipList 创建新的跳表

func (*SkipList) DeleteByID

func (sl *SkipList) DeleteByID(taskID string) *Task

DeleteByID 根据任务 ID 删除任务

func (*SkipList) Insert

func (sl *SkipList) Insert(task *Task)

Insert 插入任务

func (*SkipList) LazyDeleteByID

func (sl *SkipList) LazyDeleteByID(taskID string) *Task

LazyDeleteByID 根据任务 ID 惰性删除任务,仅移除索引并标记取消。

func (*SkipList) Len

func (sl *SkipList) Len() int

Len 返回元素数量

func (*SkipList) PeekMin

func (sl *SkipList) PeekMin() *Task

PeekMin 查看最小元素(不删除)

func (*SkipList) PopMin

func (sl *SkipList) PopMin() *Task

PopMin 弹出最小元素

func (*SkipList) PopMinBeforeOrEqual

func (sl *SkipList) PopMinBeforeOrEqual(threshold int64) *Task

PopMinBeforeOrEqual 原子弹出 ExpireAt <= threshold 的最小未取消任务。

type SkipListNode

type SkipListNode struct {
	// contains filtered or unexported fields
}

SkipListNode 跳表节点

type SparseWheel

type SparseWheel struct {
	// contains filtered or unexported fields
}

SparseWheel 稀疏时间轮

func NewSparseWheel

func NewSparseWheel() *SparseWheel

NewSparseWheel 创建新的稀疏时间轮

func (*SparseWheel) BucketsCount

func (sw *SparseWheel) BucketsCount() int

BucketsCount 返回 bucket 数量

func (*SparseWheel) Cancel

func (sw *SparseWheel) Cancel(taskID string) *Task

Cancel 惰性取消任务:移除反向索引,但保留 bucket 中的任务直到到期弹出。

func (*SparseWheel) Delete

func (sw *SparseWheel) Delete(taskID string) bool

Delete 从时间轮中删除任务 — O(1) 通过反向索引

func (*SparseWheel) DeleteTask

func (sw *SparseWheel) DeleteTask(taskID string) *Task

DeleteTask 从时间轮中物理删除任务 — O(1) 通过反向索引

func (*SparseWheel) Get

func (sw *SparseWheel) Get(tick int64) map[string]*Task

Get 获取指定 tick 的 bucket(不删除)

func (*SparseWheel) Insert

func (sw *SparseWheel) Insert(tick int64, task *Task)

Insert 插入任务到指定 tick

func (*SparseWheel) Len

func (sw *SparseWheel) Len() int

Len 返回当前任务数量

func (*SparseWheel) PeekNextTick

func (sw *SparseWheel) PeekNextTick() (int64, bool)

PeekNextTick 查看下一个有任务的 tick — O(1) 通过最小堆 返回 tick 和是否存在

func (*SparseWheel) PopTick

func (sw *SparseWheel) PopTick(tick int64) []*Task

PopTick 弹出指定 tick 的所有任务并从时间轮移除

func (*SparseWheel) PopTicksUpTo

func (sw *SparseWheel) PopTicksUpTo(maxTick int64) []*Task

PopTicksUpTo 弹出所有 <= maxTick 的任务(处理漏掉的 tick)

type Stats

type Stats struct {
	TasksExecuted  uint64
	TasksCancelled uint64
	TasksMigrated  uint64
	NearTermTasks  int
	FarTermTasks   int
}

Stats 统计信息

type Task

type Task struct {
	ID        string
	ExpireAt  int64       // 到期时间戳(毫秒)
	Interval  int64       // 周期任务间隔(毫秒),0 表示非周期
	Cancelled atomic.Bool // 取消标记(惰性删除)
	Fn        func()      // 任务函数
	// contains filtered or unexported fields
}

Task 定时任务

func (*Task) IsPeriodic

func (t *Task) IsPeriodic() bool

IsPeriodic 判断是否为周期任务

type TicklessEngine

type TicklessEngine struct {
	// contains filtered or unexported fields
}

TicklessEngine 无空转唤醒引擎

func NewTicklessEngine

func NewTicklessEngine(scheduler *Scheduler) *TicklessEngine

NewTicklessEngine 创建新的无空转唤醒引擎

func (*TicklessEngine) CalculateNextWakeTime

func (te *TicklessEngine) CalculateNextWakeTime() int64

CalculateNextWakeTime 计算下一次唤醒时间 返回毫秒时间戳,0 表示无任务

func (*TicklessEngine) NotifyIfEarlier

func (te *TicklessEngine) NotifyIfEarlier(expireAt int64)

NotifyIfEarlier 如果新的到期时间早于当前最早时间,唤醒引擎

func (*TicklessEngine) ResetEarliestExpire

func (te *TicklessEngine) ResetEarliestExpire()

ResetEarliestExpire 重置最早到期时间 在任务被执行或取消后调用

func (*TicklessEngine) SleepUntil

func (te *TicklessEngine) SleepUntil(targetTime int64) bool

SleepUntil 睡眠直到指定时间(或直到被唤醒或 context 取消) 返回 true 表示正常超时,false 表示被提前唤醒或取消

func (*TicklessEngine) WaitForTask

func (te *TicklessEngine) WaitForTask() bool

WaitForTask 等待直到有任务可执行(无任务时阻塞) 返回 false 表示 context 已取消

func (*TicklessEngine) Wake

func (te *TicklessEngine) Wake()

Wake 唤醒引擎

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL