不能直接用 std::priority_queue 做调度核心,因为它仅支持插入和取顶,缺乏抢占式调度所需的上下文管理、中断点控制和动态优先级调整能力,必须配合协作式让出、时间切片和可变堆结构(如 std::set)才能实现真正可抢占的单线程调度。

为什么不能直接用 std::priority_queue 做调度核心
因为 std::priority_queue 是被动容器,不提供「在运行中中断当前任务并切换到更高优先级任务」的能力。它只支持插入和取顶,没有「当前任务被抢占」的语义支撑——你得自己管理执行上下文、时间片、中断点,甚至模拟协程挂起/恢复。
真正关键不是排序,而是:任务如何主动让出控制权?谁来决定何时检查优先级变化?怎么避免调度开销吃掉实际执行时间?
- 所有任务必须是可重入、非阻塞的(不能出现
sleep、cin、mutex.lock()等同步等待) - 调度器本身不能依赖系统线程切换,得靠任务显式调用
yield()或在关键点轮询优先级 - 高优先级任务插入时,如果当前任务还没 yield,它就得等——除非你在每个任务的循环体内插桩检查
如何设计可抢占的执行循环(以 tick-based 为例)
单线程下“抢占”本质是协作式 + 时间切片。典型做法是把任务抽象为 std::function<void></void>,但加一层控制接口:
struct Task {
int priority;
std::function<void> run;
std::function<bool> should_preempt; // 返回 true 表示允许被更高优先级打断
};</bool></void>
调度主循环每执行一小段(比如 1000 次迭代或 1ms 耗时),就调用 should_preempt();若返回 true,立即从优先队列取新顶替任务。
-
should_preempt不应做耗时判断,推荐用原子标志位(std::atomic<bool></bool>)或简单计数器 - 避免在
run里做长循环:拆成多个小步,每步末尾检查抢占点 - 优先级变更需触发重排——
std::priority_queue不支持堆内元素修改,得用std::set或带索引的堆(如boost::heap::fibonacci_heap)
std::set 替代 priority_queue 实现动态优先级调整
当任务运行中可能提升自身优先级(例如响应外部事件),std::priority_queue 就不够用了——你没法改已入堆节点的 priority 并维持堆序。
改用 std::set<task compare></task>,其中 Compare 定义为:
struct Compare {
bool operator()(const Task* a, const Task* b) const {
return a->priority != b->priority ? a->priority > b->priority : a <p>这样就能安全地:<code>tasks.erase(old_ptr)</code> → 修改 <code>old_ptr->priority</code> → <code>tasks.insert(old_ptr)</code>。</p>
- 注意:必须用指针或智能指针,否则
set会拷贝对象,导致优先级修改失效 - 插入/删除复杂度是
O(log n),比priority_queue的O(1)插入略慢,但换来可变优先级 - 别忘了给
Task加唯一 ID 或地址比较,防止 priority 相同时比较失败
真实场景下最容易漏掉的三件事
写完能跑通 demo 很容易,但一接入真实逻辑就崩——往往卡在这些地方:
- 任务函数里调用了阻塞 I/O(比如
fread、recv),整个调度器停住:必须用非阻塞模式 +select/epoll集成,或把 I/O 封装成状态机分步执行 - 两个高优先级任务互相 yield 导致饿死低优先级任务:需要引入「最低服务配额」机制,比如记录每个优先级层级的累计执行时间,强制让低优任务至少跑满 100μs
- 任务析构时还留在
std::set里:务必在Task析构前手动erase,或者用std::shared_ptr+ 自定义删除器绑定生命周期
优先级抢占不是加个队列就行,它要求所有任务都接受被中断、所有资源访问都无锁或可重入、所有延迟都可控——否则所谓“抢占”,只是给自己埋了个不确定性的雷。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











