Less返回true表示i应比j先出队,即i更紧急;若误用“>”比较会构建逻辑最大堆导致高优任务延迟执行;同优先级需二级比较(如CreatedAt)避免饥饿。

container/heap.Less 写反了,整个调度就倒着跑
高优先级任务没被先执行,不是逻辑写错,而是 Less 返回值语义理解反了。Go 的 container/heap 是最小堆,默认把“该排前面”的元素放在堆顶;Less(i, j) 返回 true 表示 i 应该比 j 更早被 Pop() —— 也就是“i 更紧急”。
常见错误写法:return pq[i].Priority > pq[j].Priority,这实际建了个逻辑上的最大堆,但没配对使用 heap.Init 和自定义 Pop,结果就是:Priority=10 的任务永远压在 Priority=1 的下面,出队顺序完全颠倒。
- 想“数值越小越紧急”(如 0=最高优),就写
pq[i].Priority - 想“数值越大越紧急”(如 100=最高优),统一转成负数比较:
-pq[i].Priority ,别改 <code>Less逻辑去硬套 “>” - 必须加二级比较,否则同优先级任务会因堆内 swap 不稳定而饥饿:比如再比
pq[i].CreatedAt
任务动态提权后 heap.Pop 还返回旧任务
给某个已入队任务改了 Priority,比如 task.Priority = 0,但下次 heap.Pop() 拿出来的还是原来那个——这不是 bug,是预期行为。container/heap 不监听字段变化,它只认堆结构当前状态。
正确做法不是赋值完就完事,得让堆“重算位置”:
立即学习“go语言免费学习笔记(深入)”;
- 每个任务结构体里必须带
index int字段,记录它当前在堆切片里的下标 -
Swap方法里要同步更新两个元素的index值 - 改完
task.Priority后,立刻调heap.Fix(&pq, task.index),时间复杂度O(log n),比全量heap.Init快得多 - 如果忘了存
index,就没法安全调Fix;临时遍历找下标?O(n) 且并发不安全,别这么干
多个 goroutine 直接调 heap.Push/Pop 就 panic
错误信息常是 container/heap: heap invariant violated 或随机 index out of range,根本原因是 container/heap 本身零并发保护——它只管堆序,不管竞态。
所有公开操作都得包一层锁,但粒度不能乱设:
-
Push、Pop、Peek、Len全部方法入口加sync.RWMutex,读操作用RLock,写操作用Lock - 别只锁
Push,放任Peek并发读——返回的指针若被外部修改,等于绕过锁直接脏写 - 调度主循环里,锁只盖住
Pop()+ 取出任务这一步;任务执行交给 worker 异步跑,否则整个队列被阻塞 - 禁止在
Less函数里做 I/O、查 DB、调阻塞函数——它会被堆调整高频调用,一卡就拖垮全部调度
用 time.Timer 做调度触发器总 crash
最常触发 timer already fired 或 invalid memory address,问题不在 Timer 本身,而在 Reset 前没检查状态。
每次队列有变更(新任务入、堆顶变、任务取消),都要重新算下一次触发时间,并安全重置 Timer:
- Reset 前必须先
if !t.Stop() { select { case ,清掉可能残留信号 - 堆为空时别
Reset,直接Stop并置t = nil,否则 Timer 一直挂着泄漏 goroutine - Timer 回调里只负责从队列取任务、发到 worker channel,绝不直接执行
task.fn()——阻塞会导致下一轮调度延迟甚至丢帧 - 任务执行耗时不可控?加
context.WithTimeout包一层,避免单个慢任务拖死整条调度链
真正难的从来不是堆怎么建,而是当任务失败要重试、用户中途取消、优先级随业务规则实时漂移时,你怎么让 index、cancelled、execTime 这几个字段始终跟堆结构严格一致——差一个字段不同步,调度就会漏任务或重复执行。


















