























—— 从 PriorityQueue 线程安全争论,走向系统级设计
在实现定时器(Timer)时,我们常常会写出类似代码:
private PriorityQueue<TickTask, long> taskQueue;
紧接着,一个非常理性、也非常危险的问题就出现了:
❓ PriorityQueue 不是线程安全的,那我是不是应该:
- • 加锁?
- • 或换成线程安全的数据结构?
这正是大多数人会走错的第一步。
定时器不应该通过“线程安全的数据结构”来解决并发问题。
正确的解法是:
- • Timer 本体 单线程
- • 其他线程 只能投递命令
- • Timer 线程是唯一修改时间结构的地方
这不是“个人偏好”,而是被 Netty、Quartz、游戏服务器反复验证的工业结论。
我们先分析一下定时器的本质。
这意味着什么?
👉 它本质是一个“全局有序的调度器”
而“全局有序”在并发世界里,几乎天然是串行问题。
lock + PriorityQueuelock (_lock)
{
taskQueue.Enqueue(task, task.destTime);
}
问题:
👉 能跑,但不工程化
听起来很高级,但现实是:
👉 高成本,低收益
var next = tasks.Values.OrderBy(t => t.destTime).First();
这相当于:
👉 算法层面失败
这是这篇文章的关键反转点。
定时器的“并发”,到底是为了什么?
💡 注意这个区别:
并发的不是 Timer,本该并发的是“请求来源”
这正是 Netty 的 HashedWheelTimer、Quartz Scheduler、以及大多数游戏服务器的做法。

网络线程 / 逻辑线程 ConcurrentQueue 命令队列 Timer 线程 PriorityQueue / 时间轮 执行回调
并发被“压扁”为队列,复杂逻辑只存在于单线程。
interface ITimerCommand { }
record AddTaskCmd(TickTask Task) : ITimerCommand;
record CancelTaskCmd(int TaskId) : ITimerCommand;
ConcurrentQueue<ITimerCommand> commandQueue = new();
任何线程都可以安全调用:
commandQueue.Enqueue(new AddTaskCmd(task));
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 |
|---|---|---|
工程上,最优解往往不是“更强的并发”,而是“更少的并发”。
一旦你采用这种模型,你会“顺便”获得:
👉 这是系统级组件,而不是工具类
定时器不是并发问题,而是调度问题。
与其追求“线程安全的数据结构”,
不如设计一个让数据结构不需要线程安全的系统。
因为定时器本质是全局有序调度,并发只会引入复杂性。
不是,但通过单线程 Worker + 并发队列保证系统安全。
调度需要顺序一致性,并发无法提升调度性能。
使用 ConcurrentQueue 投递命令,由 Timer 线程统一处理。
技能 CD、BUFF、延迟事件、网络超时、AI 行为调度。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。