想象一下大型快递分拣中心的夜晚:几十条传送带同时运转,分拣员们站在各自的工位前,把包裹一件一件分往对应的区域。这里的核心问题是——包裹永远比人手多,但每个分拣员都必须忙而不乱。 Tokio,这个 Rust 世界最流行的异步运行时,就是这样一个精密的“分拣中心”。它的多线程调度器相当于分拣员团队,每个人都有自己的“传送带”(本地队列);协作式调度相当于一套默契规则——每处理完一个包裹就主动松手,绝不霸占工位;而 work-stealing(任务窃取)则像那个神技能:当你的传送带空了,你第一个动作不是发呆,而是从隔壁最满的传送带上“抢”几个包裹过来。 在这篇文章里,我们会像一个无人机跟拍一样,用慢镜头看清整个分拣中心的每个环节:异步运行时怎么组织工人、任务如何被“偷”走、async/await 在底层到底做了何事。你会发现,理解了这套机制之后,你写的并发代码会又快又稳,bug 也大幅减少。
对 AI 从业者而言,异步运行时是构建高性能推理服务、数据管道和 Agent 网关的地基。理解 Tokio 的调度机制,意味着你能写出吞吐量更高、延迟更稳定的 Rust 服务,这是从“能跑”迈向“能扛”的关键一步。
可视化演示
本节配有交互动效(进入视口后自动加载,约 3KB)。
Tokio运行时架构 启动运行时: #[tokio::main] async fn main() { ... } 四大组件协同工作,驱动async/await代码执行 Tokio异步运行时可视化:展示架构组件、多线程调度器、协作式调度、work-stealing算法、I/O驱动及性能调优。
异步运行时概览
快递分拣中心有个“值班经理”,他不开包、不搬货,但所有传送带的启停、所有分拣员的排班、所有突发状况的处理都归他管。在 Tokio 的世界里,这个角色就叫异步运行时(Runtime)。你写的 #[tokio::main] 注解,本质上就是告诉编译器:“给我建一个分拣中心,配好经理和员工。”
Tokio 默认启动的是多线程运行时(multi-threaded runtime),它会创建与 CPU 核心数相同的 worker 线程。每个 worker 就像一名分拣员,有自己的专属工位,也就是本地队列。任务(task)则像一个个快递包裹,被投递到各个工位前等待处理。
这里最关键的规矩是协作式调度——每个分拣员在处理完一个包裹后,必须主动把位置让出来,而不是被经理强行拽走。这保证了没有人会“霸占工位”导致别人饿死。这个规矩和操作系统里的抢占式调度(时间片轮转)截然不同,Async 世界里没有“强制下班”,全靠自觉。
回到分拣中心的比喻:经理不会招比工位还多的分拣员,人多了反而互相碍事、效率下降。理解了运行时这个“值班经理”的角色之后,我们接下来看看它手下的分拣员们(worker 线程)具体是怎么工作的。
- 符号定义
CPU核数是物理机或容器的逻辑核心数量,N_配置是开发者通过worker_threads手动指定的数值,两者取较小值。 - 取最小值的原因线程数超过 CPU 核心数时,多余线程会争抢 CPU 时间片,增加调度开销却无法提升吞吐,因此取较小值是最优策略。
多线程调度器
分拣中心里,每个分拣员都有自己的一条传送带(本地队列),但传送带之间还有一条公共中转通道(全局队列)。新包裹到达时,会被随机投到某条私人传送带上;如果私人传送带满了,才放到公共通道里等待认领。
这个设计背后有个聪明的权衡:每个 worker 优先处理自己传送带上的任务,因为“拿自己的包裹”不需要任何锁和同步,速度最快。只有当本地队列空了,worker 才去公共通道“扫货”。这种“先本地、后全局”的策略极大减少了多线程之间的竞争。
在 Tokio 源码里,每个 worker 线程维护着一个 LocalSet(本地任务集合)。当外部通过 tokio::spawn 提交任务时,运行时会把任务推送到某个 worker 的本地队列;如果当前在某个 worker 上下文内部,甚至还可以实现“同线程优先”的优化。全局队列则作为兜底,处理跨线程投递和负载溢出的情况。
这就像分拣员会先处理自己脚边的包裹,堆不下了才去公共中转区推一车过来。可问题来了:万一有个分拣员的传送带彻底空了,而旁边的人忙得满头大汗,怎么办?这就轮到我们下一节的主角——协作式调度和 work-stealing 登场了。
- 符号含义
L_i(t)表示第i个 worker 在时刻t的总待处理任务数,L_i^local是它本地队列的长度,L_i^global是它从全局队列取得的任务数。 - 队列互补当本地队列足够长时,全局队列几乎不会被动到;只有当本地队列趋近于空,全局队列才开始发挥作用,两者构成互补关系。
协作式调度
假设你是分拣员,处理一个超大的箱子——打开、检查、录入、装车,全过程十分钟,而你的传送带上还有一百个小盒子等着。按“必须处理完才能休息”的规矩,你身后排队的包裹得等到天荒地老。协作式调度的智慧在于:每处理完一个动作,就把位置让出来,让别的任务也有机会被处理。
在 async 代码里,这个“让位”的动作就是 await。当你写下 data.await 时,编译器会把 Future 拆成一个状态机,执行到这里如果数据还没准备好,Future 会返回 Pending,相当于你去跟经理说:“我的包裹要等下一班货车,先让别人做吧。” 于是 worker 线程立刻转去执行别的任务,而不是傻等。
这就是协作式调度(cooperative scheduling)的核心:任务自己决定何时让出 CPU,而不是被操作系统强抢。每个 async 任务都可以理解为 “我会尽快把控制权还给你”。正因为所有任务都守规矩,Tokio 才能在单线程上塞进成千上万个并发任务而不会卡死。
把这条铁律放回分拣中心:每个分拣员都保证“我最多连续处理 N 个包裹就让位”,于是人人都有机会轮到。但,这里有个微妙的漏洞——如果大家都守规矩,可某个人的传送带就是空的,他让不让位都一样闲。这时候需要的,就是主动出击的 work-stealing 了。
- 符号定义
T是所有任务的集合,τ是其中的任意一个任务,Δt是我们约定的时间窗口。 - 让权保证
⇒是“蕴含”符号,意即如果任务τ在执行,那么它一定会在Δt时间内让出控制权,这是约束所有任务的一条铁律。
work-stealing
分拣中心的夜班有个不成文的规矩:手上没活的人,不能干等着,必须去旁边看一眼。如果你的传送带空了,而隔壁分拣员的包裹堆成了小山,你可以直接从他的传送带尾部拿几个过来处理。这就是 work-stealing(任务窃取)。
为什么偷“尾部”而不是“头部”?想象你的邻居正在处理队首的包裹,如果你偷走队首,他回头发现包裹没了,得重新翻找,纯属捣乱。而队尾的包裹他短时间处理不到,你拿走他毫无察觉,两边都高兴。在 Tokio 中,每个 worker 的本地队列是个双向队列(deque),owner 从头部弹出任务,盗贼从尾部偷走任务——这样两边永远不会冲突,连锁都不用加。
work-stealing 带来的最大价值是天然负载均衡:线程不会闲着,而忙碌线程的队列也在不断被“削峰”。从全局看,每个任务被执行的延迟都趋于均匀,系统吞吐量因此大幅提升。这个机制也被 Go 的调度器、Java 的 ForkJoinPool 等广泛采用。
回到分拣中心:小偷不是恶意的,而是“高效地把富余资源调度到紧缺处”。这背后的功臣,正是 Rust 编译器为我们生成的 Future 状态机。接下来我们钻进代码层,看看 async/await 到底是怎么翻译成机器能听懂的动作的。
- 触发条件
L_i = 0表示第 i 个 worker 的本地队列为空,L_j > θ表示第 j 个 worker 的任务数超过阈值 θ,两者同时满足(∧)才触发窃取。 - 窃取动作从第 j 个 worker 的队尾取出 k 个任务,追加到第 i 个 worker 的队尾,两边队列长度同时更新,总任务数守恒。
async与await
分拣员处理包裹时,脑子里有个“当前进度”的记录:箱子开了一半、单据已经录入、还差最后一步装车。这个“进度记录”在 Tokio 里就是 Future 状态机。async fn 告诉编译器:“把这段逻辑改写成能记住执行到哪一步的状态机。”
每次 .await,编译器都会在状态机里插入一个“断点”。Future 被 poll(轮询)一次,就从当前位置继续往下执行一段,如果碰到还没准备好的数据,就返回 Pending,把自己挂在某个数据结构里等通知。等到数据就绪了(比如网络字节流到达),waker 会被调用,把 Future 拉回可执行队列。
这里有个微妙的技术问题:Future 内部可能持有对自身的引用(比如自引用的 struct)。如果 Future 在内存中被移动,这个引用就悬空了。为了让 Future 能安全地“记住自己”,Rust 引入了 Pin——把 Future 钉在内存中的特定位置,不让它移动。只要不移动,内部的指针就始终有效。
用分拣台来打比方:状态机就是你工位上的记事本,每次被催促(poll)就翻开笔记本,接着上次的进度多干两步;没干完就合上本子在旁边等电话(Pending + waker),电话一响(唤醒)又接着干。那从“任务被创建”到“任务被完成”,整个旅程到底经过了哪些站点?这就是任务生命周期要回答的问题。
- 状态定义
lbrace...rbrace表示分支定义,Future 的运行结果只有两种:Ready(v)携带最终值v,或Pending表示还没完成,注意这里的大小写是 Rust 标准库的惯用表达。 - poll 操作
poll(F, ctx)是运行时对 Future 的唯一驱动接口,ctx携带 waker 等上下文信息,mapsto表示函数映射,整个表达式的值就是上面的两种状态。
任务生命周期
一个包裹从进分拣中心到送上货车,要经过“接收登记”、“分拣处理”、“装车发运”三个大站。Tokio 中的任务也有相似的生命周期:创建 → 轮询 → 唤醒 → 完成。
当你调用 tokio::spawn 时,相当于在前台登记了一个新包裹。运行时把 Future 包装成一个任务(Task),分配到某个 worker 的本地队列中。接下来,worker 线程会不断对这个 Future 执行 poll。如果返回 Ready,任务完成,占用的资源被回收;如果返回 Pending,任务就在等一个事件,此时 waker 被注册到对应的事件源上。
这里的关键机制是 waker:当事件发生时(比如 socket 有数据可读),waker 会被调用,它做的事情很明确——把对应的任务重新放回可执行队列。这样 Future 的下一次 poll 才会被排上日程。Tokio 的每个 worker 都维护着一个“可唤醒任务”的集合,被唤醒的任务会再次进入某个 worker 的队列。
分拣中心的“电话回访”机制:如果某个包裹要等下一班车,前台会留一个电话(waker),车一到就打给分拣员:“你的包裹可以继续处理了!” 可是,如果这个流程里有某个环节出了岔子——比如分拣员睡了一觉,或者传送带卡住了——整个中心就会陷入混乱。这正是下面要说的常见陷阱。
- 流程起点
spawn是任务的出生点,它把 Future 放入某个 worker 的队列中,并返回一个JoinHandle句柄供后续等待结果。 - 等待与唤醒闭环
→箭头逐个连接生命周期步骤,其中Pending分支转向注册 waker,事件到达后触发 wake,任务重新进入队列准备下一次 poll,整个环表达了“等事件 → 被唤醒 → 再轮询”的循环逻辑。
常见陷阱
分拣中心最怕两件事:传送带卡死和某个分拣员罢工。Tokio 世界里对应着两个高频 bug:阻塞死锁和任务饥饿。
第一个陷阱:在 async 代码里用了 std::thread::sleep。这相当于分拣员处理包裹时直接趴在工位上睡大觉,整个线程都被他占了,其他在排队任务全部停摆。正确做法是用 tokio::time::sleep,它只会让出当前任务的控制权,不阻塞 worker 线程。
第二个陷阱:一个任务里写了死循环,永远不 await。协作式调度的基石是“任务要主动让权”,而一个大循环里如果没有任何 await 点,就等于分拣员抱着一箱货死不撒手,其他包裹全都饿死。解决办法是在热循环里适当加入 tokio::task::yield_now(),给别的任务让路。
第三个陷阱是过度拆分:把本来很简单的运算拆成几千个微小的 async 任务。每个任务都有创建、调度、回收的开销——相当于你把一个包裹硬拆成一百个碎片,分拣员光弯腰捡碎片的时间都比处理包裹本身还长。
分拣中心的管理者们当然不会坐以待毙。他们通常会做一套精细的排班和流程优化——这正是最后一节要讲的调优实践:怎样配置分拣员数量、怎样控制包裹粒度、怎样减少跨工位协作。
- 条件定义
存在表示存在一个坏任务τ,它要么睡眠时间趋近无穷,要么根本不让权,这是杀死的充分条件。 - 发酵结果
箭头右侧表示当这样的坏任务存在时,所有 worker 的队列长度都不再变化,系统进入死锁状态,这是“一个老鼠坏了一锅汤”的数学表达。
调优实践
分拣中心的绩效,最终取决于三个指标:分拣员数量是否合理、每个包裹的处理粒度是否合适、跨工位的协作成本是否够低。Tokio 的调优也是同样三个维度。
第一是 worker 线程数。默认值等于 CPU 核心数,但如果你在容器环境里运行,CPU 配额可能不是整数核。这时可以通过 worker_threads 显式配置,一般建议等于实际可用的 CPU 数。盲目开更多线程只会增加上下文切换的额外开销。
第二是任务粒度。理想情况下,一个 async 任务应该是一次“中等程度”的操作——比如处理一个 HTTP 请求或一项数据库查询。如果任务太小,调度开销会占大头;如果太大,又容易阻塞其他任务。一个常用的经验法则是:让每个任务中至少有一次 await 点,且单次执行时间不超过 1 毫秒的量级。
第三是减少跨线程共享。如果用 Mutex 包住一个高频访问的变量,每个任务访问它都要等锁,相当于分拣员每次拿包裹都要找经理签字。更好的做法是优先用消息传递(如 tokio::sync::mpsc),把共享变成“交换”,各拿各的,互不干扰。
到这儿,Tokio 这套“分拣中心”的运作全貌就清晰了:协作式调度的规矩让公平有了保障,work-stealing 让动态负载均衡成为可能,而合理的 worker 配置和任务粒度则是整体吞吐的最后一块拼图。理解了这些机制,你写的异步代码就不再是“照着模板抄”,而是真正心里有数地做架构决策。
- 吞吐定义
N_completed是单位时间内完成的任务总数,求和括起来的是所有 worker 的时间开销总和,两者相除就是系统有效吞吐量。 - 时间构成每个 worker 的时间由三部分组成:纯工作时间
t_work、迁移/被偷任务的时间t_steal、等待锁的阻塞时间t_lock。调优就是压缩后两项开销。
AI 应用场景
高性能推理网关
阶段:模型服务部署与流量调度
解决的问题:AI 推理服务时常面对突发流量,用同步阻塞模型时,每个请求占一个线程,线程数到上限后新请求排队,延迟飙升。用 Tokio 异步运行时,每个请求是一个轻量任务,worker 线程数固定为 CPU 核数,work-stealing 自动平衡各核负载,即使上万并发也能保持稳定延迟。
具体案例:某 LLM 网关服务需要同时处理多个上游推理请求。每个请求用 tokio::spawn 提交,内部对上游 HTTP 调用使用 reqwest 异步客户端。当某个上游响应慢时,任务挂起在 await 点而不是占住线程,其他请求照常处理,服务整体吞吐量提升 5-10 倍。
数据管道消费
阶段:训练数据前处理与流式消费
解决的问题:数据管道需要从 Kafka 或消息队列中持续消费大批量消息,逐条同步处理效率低且容易阻塞。用 Tokio 的 mpsc 通道 + 多 worker 消费,每个消息的处理是一个异步任务,利用协作式调度避免单条慢消息拖垮整条管道。
具体案例:一个图像分类训练数据管道需要从消息队列拉取图片路径,异步下载后做增广再写回存储。用 tokio::spawn 并发处理 1000 张图片下载,配合信号量限制最大并发数,下载耗时从串行的 30 分钟降到 40 秒。
Python 演示
"""模拟 work-stealing 调度器的最小示例"""import randomfrom collections import deque class Worker: """代表一个 worker 线程,维护本地任务队列""" def __init__(self, name): self.name = name self.tasks = deque() def add_task(self, task): self.tasks.append(task) def pop_task(self): return self.tasks.popleft() if self.tasks else None def steal_tail(self, k=1): """从队尾偷走 k 个任务(模拟 work-stealing)""" stolen = [] for _ in range(min(k, len(self.tasks))): stolen.append(self.tasks.pop()) return stolen def queue_len(self): return len(self.tasks) class WorkStealingScheduler: """简单调度器:任务入队 + 空闲时窃取""" def __init__(self, worker_names): self.workers = [Worker(n) for n in worker_names] self.global_queue = deque() self.processed_count = 0 def submit(self, task): target = random.choice(self.workers) target.add_task(task) def submit_global(self, task): self.global_queue.append(task) def run_step(self): """模拟一轮调度:每个 worker 处理一个任务,空则窃取""" for i, w in enumerate(self.workers): task = w.pop_task() if task is not None: self.processed_count += task else: # 本地队列为空:先看全局队列,再偷邻居 if self.global_queue: self.processed_count += self.global_queue.popleft() else: max_worker = max(self.workers, key=lambda x: x.queue_len()) if max_worker.queue_len() > 1: stolen = max_worker.steal_tail(1) for s in stolen: w.add_task(s) def total_pending(self): return sum(w.queue_len() for w in self.workers) + len(self.global_queue) def main(): sched = WorkStealingScheduler(["W1", "W2", "W3", "W4"]) for i in range(40): sched.submit(random.randint(1, 5)) sched.submit_global(random.randint(1, 5)) rounds = 0 while sched.total_pending() > 0 and rounds < 60: sched.run_step() rounds += 1 print(f"处理完成,总负载 {sched.processed_count},轮数 {rounds}") if __name__ == "__main__": main()
class Worker:模拟一个 worker 线程,内部用collections.deque作为双向队列,同时支持从头部弹任务和从尾部被偷任务。def steal_tail(self, k=1)从队尾弹出 k 个任务,这是 work-stealing 的关键动作:小偷只碰邻居的队尾。def run_step(self)每一轮调度,每个 worker 先处理自己的任务;队列空的 worker 先查全局队列,再对最忙邻居执行窃取。max(self.workers, key=lambda x: x.queue_len())找到当前队列最长的 worker 作为窃取目标,模拟负载均衡的“劫富济贫”策略。total_pending()返回所有队列剩余任务总数,用于判断调度是否完成。
常见误区
误区“async/await 是不是就是在一条线程上轮询?那并发性能怎么保证?Rust 的 async 是不是比多线程更弱?”
正解Tokio 默认的 runtime 是 multi_thread,会创建多个 worker 线程并行执行任务,每个线程都有自己的任务队列。async 并发不等于单线程,而是用“少量线程承载海量任务”。你甚至可以显式选择 current_thread 才能得到单线程模式,所以默认情况下 async 代码是运行在多核心上的。
为何常见:很多人把 async/await 和 JavaScript 的单线程事件循环混淆,因为语法很像,但 Rust 的 Tokio 从设计上就是多线程的。
误区“我写了 `async fn` 并且用了 `.await`,CPU 多核应该自动跑满了吧?”
正解await 只表示让权,不代表并行。你需要在代码里显式使用 tokio::spawn 或 tokio::join! 才能创建并发执行的任务。如果只是顺序 await 两个请求,它们是挨个执行的;而用 join! 同时启动它们,两个 Future 才会被同时调度。
为何常见:“async“这个词天然带给人”自动并行”的错觉,而实际上它强调的是一种让出 CPU 的机制,和并行是两码事。
误区“sleep 就是让程序等一会儿,我用标准库的 sleep 应该一样吧?反正就是暂停一下。”
正解std::thread::sleep 会阻塞整个 worker 线程,导致其他任务全部停摆,这是 Tokio 中严重的反模式,可能引发隐藏的卡顿和死锁。正确做法是 tokio::time::sleep,它基于异步定时器,只会让当前任务 yield,不会阻塞 worker 线程。
为何常见:同步编程的习惯让人下意识用熟悉的 API,没有意识到“在异步运行时里,阻塞等于停摆”的严重后果。
误区“work-stealing 是不是运行时有个中央大脑,时刻监控所有线程,然后把任务均匀分发给它们?”
正解恰恰相反,work-stealing 是完全去中心化的——没有中央调度器。每个 worker 先做自己的任务,只有在本地队列为空时,它才主动从别的线程队列尾部偷任务。这样调度成本极低(无需全局协调),又能自然地实现负载均衡。
为何常见:人们习惯性地认为“负载均衡”一定需要一个中心调度器来分配,但 work-stealing 的妙处恰恰在于“让空闲者自己动手”。
误区“只要我把一个大的计算拆成一万个小的 async 任务,吞吐就能上去了。”
正解每个任务都有创建、调度、poll、回收的固定开销。如果任务粒度太小,调度开销会远大于计算本身,反而降低吞吐。经验法则是让每个任务包含至少一次 await 点且单步执行时间在微秒到毫秒量级——就像把箱子拆成“可以单手搬动”的大小,而不是拆成粉末。
为何常见:只看到“并发”的好处,忽略了“调度”也是有成本的这个事实。
面试题
Q1. Tokio 默认创建的运行时是哪种类型?
- A.单线程 current_thread
- B.多线程 multi_thread← 正确答案
- C.无线程纯异步
- D.自适应动态线程
答案:B。Tokio 的 #[tokio::main] 默认生成 multi_thread 运行时,会创建多个 worker 线程;如果要单线程需要显式指定 flavor = "current_thread"。就像分拣中心默认配备多名分拣员,而不是只有一个人。
Q2. 在异步代码中等待一段时间,应该使用哪个 API?
- A.std::thread::sleep
- B.tokio::time::sleep← 正确答案
- C.循环忙等
- D.std::process::pause
答案:B。tokio::time::sleep 只挂起当前任务(让出线程),而 std::thread::sleep 会阻塞整个 worker 线程,让其他任务全部停摆。分拣员需要“让出工位等货”,而不是趴在工位上睡觉。
Q3. work-stealing 中,空闲 worker 一般从哪里偷取任务?
- A.邻居队列的头部
- B.邻居队列的尾部← 正确答案
- C.全局队列的头部
- D.随机挑选任何位置
答案:B。偷取邻居队列的尾部,因为队列所有者从头部弹出任务,尾部的任务对所有者来说是“暂时不碰”的,双方不会竞争。从头部偷会干扰他人的正常处理,不符合协作精神。
Q4. 协作式调度中,任务让出 CPU 的关键动作是什么?
- A.调用 thread::yield_now 并阻塞
- B.在 await 点返回 Pending← 正确答案
- C.强制结束当前线程
- D.等操作系统时间片耗尽
答案:B。协作式调度依赖 Future 在 await 点时返回 Pending 主动让权,而不是等操作系统抢占。这就像分拣员每隔几个包裹就主动说“我可以让位”,不需要经理动手拽人。
Q5. 以下哪个选项属于 Tokio 中防止任务饥饿的机制?
- A.时间片轮转抢占
- B.任务优先级队列
- C.协作式 yield + 运行时公平调度← 正确答案
- D.自动给长任务降权
答案:C。Tokio 不采用抢占式调度,而是靠每个任务“必须让权”的协作式协议,结合运行时会关注任务推进。若任务都自觉在 await/yield 让位,就天然避免了长期饥饿。分拣中心的规矩是“每处理几个就让位”,而不是靠组长去拽人。
Q6. 下列哪些情况会导致 Tokio 异步程序性能下降或卡死?
- A.在 async 中调用 std::thread::sleep← 正确答案
- B.在 async 中写死循环且无 await← 正确答案
- C.使用 tokio::spawn 创建大量任务
- D.使用 tokio::time::sleep 等待 I/O
答案:AB。A 会阻塞整个 worker 线程,B 会让协作式调度失效导致其他任务饿死。C 适度创建任务是正常的,D 是推荐做法。前两者如同分拣员睡大觉或抱货不撒手,都会让分拣中心瘫痪。
Q7. 一个 Future 被 poll 后返回 Pending 表示什么?
- A.任务已完成
- B.任务出错需要重试
- C.任务正在等待某个事件← 正确答案
- D.任务被取消
答案:C。Pending 表示 Future 还没执行完,正在等某个事件(如网络数据到达),完成后会通过 waker 被重新唤醒。相当于分拣员说“包裹在等下一班货车”,而不是说包裹丢了。
Q8. 关于 Tokio 多线程调度器的本地队列和全局队列,下列说法正确的有?
- A.worker 优先处理自己本地队列的任务← 正确答案
- B.全局队列用于跨线程投递的兜底← 正确答案
- C.所有任务必须先从全局队列分发
- D.本地队列从头部取任务,偷窃者从尾部取← 正确答案
答案:ABD。worker 优先处理本地队列(A 对),全局队列是兜底(B 对),任务多数直接进本地队列而不是全部走全局(C 错),本地队头自用、队尾可被偷(D 对)。这就像分拣员先处理自己脚边的包裹,不够了才去公共区域拿。
Q9. async 块被编译后,编译器生成的“执行进度记录”叫什么?
- A.线程栈帧
- B.Future 状态机← 正确答案
- C.全局调度器
- D.事件循环句柄
答案:B。async 块里的每个 .await 都会变成状态机里的一个状态,记录执行到哪一步。相当于分拣台边的“进度记事本”,每次恢复执行时翻开接着干,不用从头再来。
Q10. 关于 Tokio 调优,以下哪些做法是推荐的?
- A.worker_threads 设置为 CPU 核心数← 正确答案
- B.任务粒度保证至少含一个 await 点← 正确答案
- C.高频共享数据用全局 Mutex 保护
- D.优先用 mpsc 通道传递数据← 正确答案
答案:ABD。worker 数量设为 CPU 核数是合理默认(A 对),任务内有 await 才是“好的任务”(B 对),高频 Mutex 会引发锁竞争应避免(C 错),通道传递能减少共享与等待(D 对)。调优的本质就是减少“排队等锁”和“无效调度”的时间。