
Work-Stealing 调度器本地队列与全局队列的工作窃取机制在多核高并发系统中如何将海量的微小异步任务Task均匀分发到各个 CPU 物理核心上是决定异步运行时吞吐上限的核心难题。如果采用最简单的**单全局共享队列Single Global Queue**架构所有 Worker 线程在每次获取新任务时都必须去抢占全局队列的互斥锁。当并发任务量达到数十万时多核心之间的缓存一致性协议MESI 广播与锁争用会直接把 CPU 算力全部消耗在自旋与总线锁上。为了解决这一吞吐瓶颈现代高性能异步运行时如 Tokio、Go Runtime、Java ForkJoinPool无一例外地采用了工作窃取算法Work-Stealing Scheduling。理解 Tokio 中本地环形队列Local Run Queue、全局注射队列Global Injection Queue与窃取机制的协同设计是排查高负载下任务调度倾斜与毛刺的关键。-------------------------------------------------------------------------- | Tokio Work-Stealing 调度拓扑 | -------------------------------------------------------------------------- | 全局注射队列 (Global Injection Queue / MPMC 慢速保底) | | [ Task G1 ] - [ Task G2 ] - [ Task G3 ] ... | -------------------------------------------------------------------------- ^ ^ | 周期性轮询 (如每 61 次迭代) | 队列溢出溢出回退 v v ----------------------------- ----------------------------- | Worker 0 (CPU 核心 0) | 窃取 | Worker 1 (CPU 核心 1) | | 本地无锁环形队列 (Local 256) | | 本地无锁环形队列 (Local 256) | | [T1] [T2] [T3] ... | 一半 | [ 空闲空转中... ] | ----------------------------- -----------------------------1. 本地队列设计无锁单生产者-多消费者环形缓冲区Tokio 为每个 Worker 线程分配了一个固定长度默认 256 槽位的本地运行队列Local Run Queue。这个本地队列具有独特的并发访问模式本地 Worker 线程是唯一的生产者与主消费者本地 Worker 从队列头部Head取任务执行并将新生成的本地 Task 压入队列尾部Tail。在这个独占路径上操作完全不需要加锁通过无锁的原子游标更新即可在几个纳秒内完成其他空闲 Worker 是并发的偷取者Stealers当其他 Worker 线程自身队列变空时它们会扮演消费者从该队列中并发“偷取”任务。巧妙的 256 容量限制与溢出处理为什么本地队列的长度被固定为 256而不是无限制动态扩容的数组防止单个 Worker 发生内存饥饿与倾斜如果一个任务内部疯狂递归生成子任务256 的容量上限能防止这些子任务全部积压在单个 Worker 核心上溢出回退到全局队列当本地队列被填满 256 个任务时Worker 会将本地队列中**一半的任务128 个打包一次性转移到全局注射队列Global Queue**中主动让出给其他 Worker 分担。2. 工作窃取流程半数批量窃取Steal Half当 Worker A 发现自己的本地队列变空时它不会立刻陷入休眠Sleep而是进入窃取状态机随机挑选受害者Victim Selection为了避免所有空闲 Worker 同时盯上同一个繁忙 Worker 造成二次争锁Worker A 会随机选择一个目标 Worker B批量窃取一半任务Steal HalfWorker A 不会只偷 1 个任务而是通过 CAS 操作尝试一次性从 Worker B 的队列头部偷取其当前积攒任务总数的50%最多 128 个并直接搬迁到自己的本地队列中消除颠簸Anti-Thrashing一次性偷取一半任务保证了 Worker A 在接下来的几百微秒内拥有充足的工作储备不需要频繁触发昂贵的跨核心窃取。// 伪代码工作窃取核心逻辑 impl Worker { pub fn fetch_next_task(mut self) - OptionTask { // 1. 优先消费本地最高优先级的 LifoSlot if let Some(task) self.lifo_slot.take() { return Some(task); } // 2. 从本地无锁队列中弹出任务 if let Some(task) self.local_queue.pop() { return Some(task); } // 3. 周期性每 61 次迭代主动检查全局队列防止全局任务被饿死 if self.tick % 61 0 { if let Some(task) self.global_queue.pop() { return Some(task); } } // 4. 本地为空进入跨核心窃取逻辑 self.steal_from_peers() } fn steal_from_peers(mut self) - OptionTask { let victims self.get_randomized_peers(); for peer in victims { // 尝试从 peer 队列偷取一半任务 if let Some(stolen_task) peer.local_queue.steal_half_into(mut self.local_queue) { return Some(stolen_task); } } // 5. 最后兜底检查全局队列 self.global_queue.pop() } }3. LIFO Slot 优化极致的 CPU 缓存局部性Tokio 还有一个极其精妙的微架构优化——LIFO Slot后入先出单槽位。当一个正在执行的 Task A 通过tokio::spawn产生了一个新 Task B 时调度器不会把 Task B 扔进队列尾部而是将其暂存在一个名为lifo_slot的单任务寄存器式变量中。当 Task A 执行完当前的这一轮 Poll 后Worker 线程会优先取出lifo_slot中的 Task B 立即执行为什么这样做能大幅降低延迟因为 Task B 刚刚被 Task A 创建Task B 所需的数据包括请求结构体、张量切片在 CPU 的 L1/L2 Cache 中处于完全温热Hot Cache状态立即执行 Task B 可以实现极致的 CPU 缓存命中避免了将任务推入队列尾部再由其他核心冷加载读取导致的 Cache Miss。通过本地无锁环形队列、批量偷取一半策略以及 LIFO Cache 局部性插槽的三位一体配合Work-Stealing 架构在多核心硬件上构建起了一套既能各自狂飙、又能自动动态削峰填谷的终极调度秩序。