惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

酷 壳 – CoolShell
酷 壳 – CoolShell
H
Hacker News: Front Page
P
Palo Alto Networks Blog
T
ThreatConnect
Apple Machine Learning Research
Apple Machine Learning Research
博客园_首页
T
True Tiger Recordings
P
Privacy & Cybersecurity Law Blog
B
Blog
IT之家
IT之家
Last Week in AI
Last Week in AI
F
Full Disclosure
Hacker News: Ask HN
Hacker News: Ask HN
C
Comments on: Blog
Microsoft Azure Blog
Microsoft Azure Blog
C
Cybersecurity and Infrastructure Security Agency CISA
Microsoft Security Blog
Microsoft Security Blog
博客园 - 【当耐特】
N
News and Events Feed by Topic
NISL@THU
NISL@THU
腾讯CDC
雷峰网
雷峰网
Security Latest
Security Latest
李成银的技术随笔
M
Microsoft Research Blog - Microsoft Research
L
LangChain Blog
L
Lohrmann on Cybersecurity
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
C
Check Point Blog
Y
Y Combinator Blog
Recent Announcements
Recent Announcements
博客园 - Franky
N
News | PayPal Newsroom
V
V2EX
A
About on SuperTechFans
The Register - Security
The Register - Security
月光博客
月光博客
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Google Online Security Blog
Google Online Security Blog
MyScale Blog
MyScale Blog
Cisco Talos Blog
Cisco Talos Blog
Vercel News
Vercel News
WordPress大学
WordPress大学
C
Cyber Attacks, Cyber Crime and Cyber Security
The Hacker News
The Hacker News
IntelliJ IDEA : IntelliJ IDEA – the Leading IDE for Professional Development in Java and Kotlin | The JetBrains Blog
IntelliJ IDEA : IntelliJ IDEA – the Leading IDE for Professional Development in Java and Kotlin | The JetBrains Blog
爱范儿
爱范儿
A
Arctic Wolf
L
LINUX DO - 最新话题
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More

博客园 - 世纪末の魔术师

坚毅,是一种缓慢修理自己的方式 语言的边界,与软件的命运 人类与AI协同进化 《坚毅》第一部分读书笔记 用 System.CommandLine 构建工程级 CLI 工具 用 Command 模式构建可扩展的命令行工具 从哎呦”到语言宇宙 ——读《What Is ChatGPT Doing … And Why Does It Work?》 ⏱️ 深入理解定时器中的【时间轮算法】 C# AOT编译后——调用其类库方法因顺序出错? UnitTask中的Forget()与 CTS 光线追踪和球体追踪 八、方法(method) 二十、异常与状态管理(Exception&State Management) 二十八、IO绑定的异步操作(IO-Bound Async) 二十二、CLR寄宿与AppDomain(CLR Hosting and App Domains ) 二十九、原始线程同步构造(Primitive Thread Synchronization Constructs ) 二十六、线程与并发(Thread Basic) 二十七、计算密集型异步操作(Compute-Bound Asynchronous Operations) 二十三、程序集加载与反射(Assembly Loading and Reflection)
🚫 为什么「定时器」不应该是线程安全的?
世纪末の魔术师 · 2025-12-25 · via 博客园 - 世纪末の魔术师

image

🚫 为什么「定时器」不应该是线程安全的?

—— 从 PriorityQueue 线程安全争论,走向系统级设计

一、问题的起点:一个“看起来很合理”的疑问

在实现定时器(Timer)时,我们常常会写出类似代码:

private PriorityQueue<TickTask, long> taskQueue;

紧接着,一个非常理性、也非常危险的问题就出现了:

❓ PriorityQueue 不是线程安全的,那我是不是应该:

  • • 加锁?
  • • 或换成线程安全的数据结构?

这正是大多数人会走错的第一步

二、先说结论(很重要)

定时器不应该通过“线程安全的数据结构”来解决并发问题。

正确的解法是:

  • • Timer 本体 单线程
  • • 其他线程 只能投递命令
  • • Timer 线程是唯一修改时间结构的地方

这不是“个人偏好”,而是被 Netty、Quartz、游戏服务器反复验证的工业结论

三、为什么“线程安全 PriorityQueue”是个伪命题?

我们先分析一下定时器的本质

定时器在做什么?

  • • 管理 未来时间点
  • • 决定 哪个任务先执行
  • • 保证 严格的时间顺序

这意味着什么?

👉 它本质是一个“全局有序的调度器”

而“全局有序”在并发世界里,几乎天然是串行问题

四、三种“直觉解法”,为什么都不优雅?

❌ 方案一:lock + PriorityQueue

lock (_lock)
{
    taskQueue.Enqueue(task, task.destTime);
}

问题:

  • • Tick 线程可能被阻塞
  • • 回调里再 AddTask → 死锁风险
  • • 锁竞争严重
  • • Timer 精度和稳定性下降

👉 能跑,但不工程化

❌ 方案二:自己实现 ConcurrentPriorityQueue

听起来很高级,但现实是:

  • • .NET 没有官方并发堆
  • • 实现极复杂
  • • 并发 Bug 极难排查
  • • 性能未必比单线程好

👉 高成本,低收益

❌ 方案三:ConcurrentDictionary + 每 Tick 排序

var next = tasks.Values.OrderBy(t => t.destTime).First();

这相当于:

  • • 每一帧重建一个堆
  • • 时间复杂度倒退

👉 算法层面失败

五、换个角度:定时器真的需要“并发”吗?

这是这篇文章的关键反转点

问一个反问题:

定时器的“并发”,到底是为了什么?

  • • 是为了提高执行速度?❌
  • • 是为了提高吞吐?❌
  • • 是为了安全接收来自多个线程的请求?✅

💡 注意这个区别

并发的不是 Timer,本该并发的是“请求来源”

六、正确模型:单线程 Timer + 并发投递

这正是 Netty 的 HashedWheelTimer、Quartz Scheduler、以及大多数游戏服务器的做法。

架构图(重点)

image

网络线程 / 逻辑线程 ConcurrentQueue 命令队列 Timer 线程 PriorityQueue / 时间轮 执行回调

核心思想一句话:

并发被“压扁”为队列,复杂逻辑只存在于单线程。

七、Unity / C# 中的推荐实现骨架

1️⃣ 命令模型(非常关键)

interface ITimerCommand { }

record AddTaskCmd(TickTask Task) : ITimerCommand;
record CancelTaskCmd(int TaskId) : ITimerCommand;

2️⃣ 并发投递队列

ConcurrentQueue<ITimerCommand> commandQueue = new();

任何线程都可以安全调用:

commandQueue.Enqueue(new AddTaskCmd(task));

3️⃣ Timer Update(唯一操作堆的地方)

void UpdateTimer()
{
    // 1. 合并并发请求
    while (commandQueue.TryDequeue(out var cmd))
    {
        switch (cmd)
        {
            case AddTaskCmd add:
                taskQueue.Enqueue(add.Task, add.Task.destTime);
                break;
            case CancelTaskCmd cancel:
                canceledSet.Add(cancel.TaskId);
                break;
        }
    }

    // 2. 处理到期任务
    long now = GetNowMilliseconds();

    while (taskQueue.Count > 0 &&
           taskQueue.Peek().destTime <= now)
    {
        var task = taskQueue.Dequeue();
        if (canceledSet.Contains(task.tid))
            continue;

        task.taskCB?.Invoke();
    }
}

📌 注意:

  • • PriorityQueue完全不需要线程安全
  • • Timer 行为 完全可预测
  • • 并发复杂度降为 O(1)

八、为什么这是“最好的解决方案”?

正确性 难证明 极强 性能 锁竞争 无锁 调试 地狱 简单 扩展性 差 极好 工业验证 少 大量
维度并发堆单线程 Timer

工程上,最优解往往不是“更强的并发”,而是“更少的并发”。

九、这套设计的隐藏价值(高级)

一旦你采用这种模型,你会“顺便”获得:

  • • Cancel / Pause / Resume(命令化)
  • • 任务回放 / 调试(记录命令)
  • • 网络同步(序列化命令)
  • • 时间轮 / 堆 / 混合策略自由切换
  • • ECS / JobSystem 友好

👉 这是系统级组件,而不是工具类

十、终极总结(可以直接放在文章结尾)

定时器不是并发问题,而是调度问题。

与其追求“线程安全的数据结构”,
不如设计一个让数据结构不需要线程安全的系统

🎯 Unity / 架构面试高频题(含答案)

1️⃣ 为什么 PriorityQueue 不适合做线程安全定时器?

因为定时器本质是全局有序调度,并发只会引入复杂性。

2️⃣ Netty 的时间轮是线程安全的吗?

不是,但通过单线程 Worker + 并发队列保证系统安全。

3️⃣ 为什么 Timer 适合单线程?

调度需要顺序一致性,并发无法提升调度性能。

4️⃣ 如何安全地在多线程中添加定时任务?

使用 ConcurrentQueue 投递命令,由 Timer 线程统一处理。

5️⃣ Unity 中这种 Timer 设计适合哪些系统?

技能 CD、BUFF、延迟事件、网络超时、AI 行为调度。