想象你是一家繁忙咖啡店的唯一咖啡师。早晨高峰,20 个顾客同时下单:美式、拿铁、手冲……如果你坚持“一次只做一杯”,做那杯手冲(要磨豆、焖蒸、注水,整整 4 分钟)时,其他 19 位顾客就只能干等。但聪明的咖啡师会这样干:手冲正在滴滤时,转身去把美式做了;美式在接咖啡液时,又去给拿铁打奶泡——每一秒都没闲着。 这种“一件事在等待时,转头去做另一件事”的能力,就是协程调度的灵魂。Python 的 asyncio 就像一个训练有素的咖啡师调度系统,它知道每个订单(协程)进行到哪一步、谁在等什么资源、谁可以暂停、谁需要立刻恢复。理解协程调度,就是读懂这位咖啡师的排单心法:什么时候挂起、什么时候恢复、如何决定先做哪杯、如何保证没有客人被无限晾着。
掌握协程调度,你就能在高并发 IO 场景下用极少量线程撬动海量并发任务,让代码在“等待”中榨干每一滴 CPU 能力——这是后端开发进阶的必修内功。
可视化演示
演示Python协程调度的核心机制,包括await挂起恢复、任务优先级、并发控制、信号量限流和公平调度,通过交互式可视化展示协程在事件循环中的执行流程。
本节配有交互动效(进入视口后自动加载,约 3KB)。
Await挂起恢复 遇到await挂起当前协程 保存执行状态到协程对象 事件循环调度其他就绪任务 await是协程调度的核心机制
调度基础
咖啡师小明的台面上贴着一张便利贴菜单,上面写着所有待做订单。他不需要记住每一杯的操作细节——只要看菜单就知道:3 号美式在等浓缩液、7 号拿铁在等蒸汽、9 号手冲在滴滤。这张便利贴,就相当于事件循环(Event Loop)里的任务队列;每个订单就是队列中的一个协程。
Python 中 async def 定义的函数被调用时并不会执行,而是返回一个协程对象——就像顾客点单后拿到的小票,上面写着“将做一杯美式”,但还没开始做。要让协程真正跑起来,需要把它包成 Task(任务)扔进事件循环。asyncio.create_task() 就是这个动作:把小票交给咖啡师,咖啡师把它贴到便利贴上排队。
事件循环是这个调度系统的核心引擎,它维护两类集合:就绪队列(可以立刻做的订单)和等待队列(在等咖啡机/牛奶/热水的订单)。循环每转一圈,就从就绪队列取一个任务执行,遇到等待条件就把任务扔进等待队列,然后转头处理下一个就绪任务。
调度基础就像咖啡师的便利贴系统:事件循环是咖啡师本人,就绪队列是“在台面上直接能做的订单”,等待队列是“需要等机器的订单”。没有这个基础,咖啡师只会手忙脚乱——理解了它,你就能看懂后面所有调度行为的底层逻辑。
while True 的轮询器,它维持任务队列不断切换执行,让所有任务公平地推进。- 事件循环取任务Python 中
loop.run_until_complete()启动事件循环,循环从就绪队列取出一个可执行的 Task。 - 执行协程直到等待对 Task 调用其协程的
send(None)方法,协程会一直执行到遇到下一个await表达式,然后暂停。 - 切入等待队列
await的可等待对象(如 Socket IO)如果尚未就绪,Task 会被注册到事件循环的等待队列中,同时协程状态被保存。 - IO 完成后回归就绪当 IO 事件到达(如数据可读),事件循环该 Task 移回就绪队列,等待下一轮被调用。
await挂起恢复
“等待”是协程调度的灵魂时刻。小明做美式时发现浓缩机还在预热,他没有傻站着,而是把美式订单翻个面(标记“等在预热”),转手去做拿铁——先打奶泡。这个“翻面”动作就是协程的挂起,对应的代码就是 await。
await 的关键语义是挂起而不阻塞。它告诉事件循环:“当前这个任务需要等一个东西(IO、定时器、另一个协程),在此期间请先执行别的任务。” 注意,挂起的是当前协程,而整个事件循环的线程并没有停下来——咖啡师本人没有闲着,他还在做别的订单。
挂起时,协程的完整状态(局部变量、指令指针、调用栈)会被保存在 Task 对象中。这就像给订单夹上一个便签:“美式,浓缩机预热到 90°C 后,加 1 泵糖浆,用 350ml 杯。” 恢复时,事件循环通过调用 coroutine.send(value) 从上次暂停的位置继续执行。
await 挂起与恢复,就是咖啡师“不傻等”的秘诀。每个订单被翻面等待时,那杯咖啡的进度被完整记录下来,等机器就绪后咖啡师拿起便签接着做——没有一步操作被遗漏。在你的代码里,每一次 await 都是在告诉事件循环:“这里可能要等,先安排别人,我很快就回来。”
- 执行到 awaitPython 解释器执行协程代码时碰到
await,这是一个“我可能要等”的信号。 - 检查可等待对象Python 调用
A.__await__()获取迭代器,若A尚未完成,协程会暂停执行。 - 保存状态并挂起当前帧的局部变量、行号等被打包保存到 Task 对象,Task 被加入等待队列。
- IO 完成恢复执行事件循环收到 IO 就绪通知,调用
task.coro.send(result),协程从挂起点继续。 - 继续运行await 表达式求值得到结果,协程继续执行后续代码,直到下一个 await 或函数结束。
任务优先级
咖啡店有 VIP 客户,也有赶时间的外卖骑手。小明在便利贴菜单上会用红色马克笔标出急单,这些订单会提前做。在协程调度中,这种“急单机制”通过 asyncio.wait 的 timeout 参数和 asyncio.wait_for 来实现,但更底层的优先级逻辑依赖 asyncio 的 Queue 配合 PriorityQueue 自定义实现。
Python 原生 asyncio 的 Task 本身没有公开的优先级属性,但我们可以通过调度顺序控制来影响优先级:先 create_task 的任务通常先被调度(FIFO),而 loop.call_soon() 可以插队到队首。此外 asyncio.sleep(0) 是一个“让位”技巧,主动把执行权交还给事件循环,让其他同优先级任务有机会运行。
对于需要严格优先级的场景,更实用的方式是结合 asyncio.Queue 的优先级变体:
优先级机制就像咖啡师的红色马克笔。不是所有订单都一样紧急——VIP、外卖、平台超时单需要优先处理。你的代码也一样:用 PriorityQueue 把紧急任务排到队首,让调度器优先服务。注意,这件事是“调度顺序”的体现,和操作系统进程优先级的内部抢占机制不同;asyncio 的协作式模型下,高优任务是“被先取到并执行”,而不是“打断别人”。
- 按优先级入队任务进入
PriorityQueue时按 (priority, task) 元组存储,底层用堆排序维护。 - 取队首(最小优先级号)事件循环从这里拿任务时,总拿到堆顶的最低编号,例如 p=1 的高优任务。
- 执行该任务拿到任务后对其
await完成,结束后回到取任务循环,继续取下一个最高优先级任务。
import asynciofrom asyncio import PriorityQueue async def worker(q): while True: priority, coro = await q.get() await coro q.task_done() q = PriorityQueue()await q.put((1, task_a())) # 数字越小优先级越高await q.put((10, task_b()))
调度器策略
小明的老板给他定了一条规矩:绝不和任何顾客纠缠超过 30 秒。如果一个订单太复杂,先做到一半停下来,做完其他简单订单再回来继续。这在计算机调度中叫时间片轮转(Round-Robin)。而 asyncio 的调度策略有自己的特色——它是协作式(Cooperative)的:切换不是靠外部打断,而是靠每个协程自觉 await。
asyncio 的默认调度策略是协作式 + FIFO 队列:每个就绪任务按被创建的先后排列,事件循环依次调用它们,直到每个任务遇到 await 才让位。这种策略的优点是切换开销极小(没有操作系统级上下文切换,只是 Python 函数暂停/恢复),缺点是某个协程如果不 await 就长时间占用 CPU,整个循环会被打断。
这是 asyncio 和线程调度最本质的区别:线程调度是抢占式的(操作系统强制切走),协程是协作式的(本人主动让位)。
调度策略就像咖啡团队的工作章程:协作式让每个咖啡师自行安排节奏,时间片轮转保证没有顾客被无限冷落。理解这一点至关重要——在线程世界里,“不干活”是会被系统打断的;在协程世界里,你必须在代码里主动 await 让位,否则整个异步系统都会卡壳。协作式的代价是“自觉”,好处是“零切换开销、超高并发吞吐”。
- 创建任务进入运行队列事件循环运行时,任务按创建顺序放入内部就绪队列。
- 顺序执行每个任务事件循环从队首取出任务,驱动其执行,直到遇到 await。如果 await 的对象未就绪,任务进入阻塞集合;如果已就绪,则继续执行。
- 就绪任务回到队尾被 await 的数据准备好后,任务回到就绪队列尾——公平轮转,人人有机会。
并发控制
咖啡店把订单交给三个咖啡师分别做。老板需要知道三件事:所有订单都做完没有?,哪个订单先做完?,同时最多允许几个订单在操作?——对应 asyncio 的 gather、wait 和信号量(Semaphore)。
asyncio.gather 是最常用的并发等待工具。它接收多个协程或任务,并发地运行它们,然后等待全部完成,并返回每个任务的结果列表。如果其中一个任务抛出异常,gather 会默认把异常抛给调用方,而且不会等待其他任务完成——这就像咖啡师看到某杯咖啡打翻了,立刻全局报错。
asyncio.wait 更灵活:它支持 return_when=asyncio.FIRST_COMPLETED 或 ALL_COMPLETED,返回 (done, pending) 两个集合。这就像老板不需要等所有订单完成,可以先处理第一批完成的外卖单。但要注意,wait 对异常的处理更温和——它不会抛异常,而是把任务放进 done 集合里等你自己检查。
Semaphore(信号量) 是最实用的并发控制工具,它能限制同时运行的任务数量,起到限流作用。
并发控制是调度系统给咖啡师的“管理仪表盘”:gather 告诉他“这桌订单全部齐了再上菜”,wait 告诉他“哪个先好就先端哪个”,Semaphore 告诉他“后厨同时最多开 3 个灶,别把厨房挤爆”。在实际项目中,gather 常用于批量请求多个 API 并汇总结果,wait 常用于“只要有第一个成功响应就开始做后续处理”,Semaphore 则是保护数据库不会被打爆的必备利器。
- 创建任务集合gather 会把传入的协程先包装成 Task,统一调度。
- 等待集合中所有任务完成内部生成一个共享的 Future,当所有任务都 done 时该 Future 完成。
- wait 的 FIRST_COMPLETED 分支只要有一个任务完成,立即返回 done 集中已完成的子集,pending 中包含其余未完成任务。
- Semaphore 限流每个任务在进入临界区前先获取许可(若超出计数则等待),完成后释放。
避免协程饿死
小明犯过一个错误:他老是优先做那些快的、简单的订单,结果有个大单(手冲+拉花+三层蛋糕)一直排在最后,客人等了 50 分钟气走了。这就是饿死(Starvation)——某个任务因为优先级太低或调度不公,一直得不到执行机会。
在 asyncio 中,饿死通常有两个典型原因。原因一:某个协程是死循环且从不 await,导致事件循环永远无法执行其他就绪协程——它是“霸占咖啡师的客人”;原因二:高优先级任务源源不断补充,低优先级任务永远排在末尾——它是“被 VIP 不断插队的普通顾客”。
解决方案有三板斧。第一板斧:公平轮转——确保每个协程都定期 await 让位,让其他协程获得 CPU;第二板斧:超时保护——用 asyncio.wait_for(coro, timeout) 给每个任务设定最大等待时间,超时就放弃或重试;第三板斧:任务混排——在使用 PriorityQueue 时对相同优先级的任务做轮转,避免极端倾斜。
避免饿死,是不让任何一杯咖啡被永久遗忘。公平轮转让每个任务都有发言机会,超时保护让失控任务被强制叫停,任务混排让优先级队列不至于变成“VIP 专用通道”。写协程代码时,时刻反问自己:我的某个低优任务会不会永远等不到调度?我的某个高优协程是不是从未让出过 CPU?—— 这就是协程调度的“面试官之问”,也是最容易在生产环境踩坑的地方。
- 任务持续等待低优先级任务每次调度都被高优任务抢在前面,它的等待时间不断增长。
- 触发饥饿保护当等待时间超过阈值,调度器介入,临时将这个任务的优先级提到队首。
- 超时强制放弃或使用 wait_for 包裹,超时后取消协程并抛出 TimeoutError。
- 定期让位协程内主动添加
await asyncio.sleep(0),保证事件循环有其他任务被调度的机会。
AI 应用场景
场景一:高并发爬虫限流调度
阶段:数据采集与预处理
解决的问题:传统爬虫每请求一个页面就阻塞等待响应,串行采集 1000 个 URL 要几十分钟。即使改为多线程,线程上下文切换开销大,且对目标网站压力不可控,容易触发反爬封禁。
具体案例:某 AI 训练数据服务商需要每天从 50 个新闻网站抓取 10 万篇文章。使用 asyncio.gather 并发发起请求,同时用 asyncio.Semaphore(20) 限制同时最多 20 个 HTTP 请求,每个请求设置 wait_for 10 秒超时。抓取速度提升 40 倍,服务器负载平稳,零封禁。
场景二:实时推理服务任务调度
阶段:模型推理与 Serving
解决的问题:AI 在线推理服务同时接收大量用户请求,若串行处理,GPU 利用率低且响应延迟高;若直接无限制并发,GPU 显存和 CPU 计算资源会被瞬时打满,导致雪崩。
具体案例:某推荐系统推理服务使用自建协程调度器:请求进来后包装成协程任务,按用户等级标记优先级(VIP 用户进高优队列),用 Semaphore 限制并发数为 GPU 批处理最优值(如 32),并用 gather 收集一个 batch 的推理结果后统一返回。P99 延迟从 800ms 降到 120ms,GPU 利用率从 30% 提升到 90%。
场景三:AI Agent 多任务并行规划
阶段:Agent 编排与任务执行
解决的问题:AI Agent 在执行复杂任务时,经常需要同时调用多个工具(搜索、代码执行、数据库查询、模型调用),且部分工具响应慢。若串行调用,Agent 思考链路过长;若随意并发,结果时序混乱且资源浪费。
具体案例:个人 AI 助理在回答“帮我对比三款显卡并推荐”时,用 asyncio.gather 同时发起三个商品搜索请求,用 wait(FIRST_COMPLETED) 优先处理最先返回的结果,并为每个工具调用设置超时(防止模型等待过久)。整个回答链路从 15 秒缩短到 4 秒,用户体验大幅提升。
Python 演示
import asyncioimport randomimport time async def fetch_data(url, delay): """模拟一个 IO 请求:网络等待 + 数据处理""" print(f"开始请求: {url}") await asyncio.sleep(delay) # 模拟网络延迟 result = f"{url} -> 数据({delay}s)" print(f"完成请求: {url}") return result async def limited_fetch(sem, url, delay): """带信号量保护的请求,防止并发过大""" async with sem: # 获取许可,超过并发数则等待 return await fetch_data(url, delay) async def main(): urls = [ ("https://api.a.com/item", 0.5), ("https://api.b.com/item", 1.0), ("https://api.c.com/item", 0.3), ("https://api.d.com/item", 0.8), ] # 1. gather: 并发执行所有任务,等待全部完成 print("=== gather 演示 ===") results = await asyncio.gather( fetch_data("https://g-1.com", 0.4), fetch_data("https://g-2.com", 0.6), ) print("全部完成:", results) # 2. wait: 只要有最先完成的任务就处理 print("\n=== wait 演示 ===") tasks = [asyncio.create_task(fetch_data(u, d)) for u, d in urls] done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED) for t in done: print("最先完成:", t.result()) for t in pending: t.cancel() # 取消未完成的任务 print("剩余已取消:", len(pending)) # 3. Semaphore: 限制最大并发数为 2 print("\n=== 信号量限流演示 ===") sem = asyncio.Semaphore(2) tasks2 = [limited_fetch(sem, u, d) for u, d in urls] completed = await asyncio.gather(*tasks2) print("限流完成:", completed) # 4. wait_for: 超时保护,防止任务饿死 print("\n=== 超时保护演示 ===") try: await asyncio.wait_for(fetch_data("https://slow.com", 5), timeout=1) except TimeoutError: print("任务超时,已强制取消") if __name__ == "__main__": asyncio.run(main())
await asyncio.sleep(delay)模拟网络 IO 等待,主动让出执行权给事件循环asyncio.gather(...)并发执行多个协程,返回所有结果组成的列表asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)只等最快的任务先返回,不阻塞等待全部完成asyncio.Semaphore(2)同时最多允许 2 个协程进入临界区,防止并发过高asyncio.wait_for(future, timeout=1)给协程设定超时,超过 1 秒就强制取消,避免任务卡死
常见误区
误区我一直以为 `await asyncio.sleep(1)` 会像 `time.sleep(1)` 一样把程序卡住一秒钟,用 await 和不用 await 好像没区别,程序照样是慢吞吞地一个个执行。
正解恰恰相反,await 是“让位不阻塞”。当协程执行到 await 时,它会把执行权交还给事件循环,事件循环立刻去执行其他就绪的协程。就像咖啡师碰到浓缩机预热,不是傻站一分钟,而是立刻去做别的单。time.sleep 是“整个人定住不能干活”,await asyncio.sleep 是“这个订单暂停,人去做别的”。
为何常见:初学者从同步编程转型,习惯了“代码一行行顺序执行”的心智模型,难以理解“暂停后还能跳去执行别处”的控制流反转。
误区协程听起来很强大,那我用它来并行计算几个大数的斐波那契数列,应该能快好几倍吧?毕竟“并发”嘛。
正解协程只能在 IO 等待时切换,不能利用多核 CPU。协程本质是单线程内的协作式调度,如果任务是纯 CPU 计算且不 await,整个事件循环会被卡住,其他协程完全没机会执行。加速 CPU 密集计算要用多进程(ProcessPoolExecutor)或多线程;协程的舞台是 IO 密集场景(网络请求、文件读写、数据库查询)。
为何常见:把“并发”和“并行”混淆了。并发是逻辑上同时推进(交替执行),并行是物理上同时执行(多核)。协程是并发的高手,不是并行的替代品。
误区gather 和 wait 不都是等任务完成吗?那我随便哪个方便用哪个,反正效果一样。
正解二者行为差异很大。gather 会在任一任务抛异常时直接向上抛(默认 cancel 其他任务);wait 不抛异常,而是把所有任务(包括异常/完成的)放进 done 集合,需要你自己检查 task.exception()。gather 返回结果列表(顺序固定),wait 返回 (done, pending) 集合(无序)。选错了,异常处理和结果获取逻辑会完全走样。
为何常见:只看两者的“等待完成”表象,忽略了错误传播和返回值结构的本质区别。
误区我加了 `asyncio.Semaphore(100)` 让并发数量变大,程序应该跑得更快吧?把信号量设得越大越好。
正解Semaphore 是限流器,不是加速器。它约束的是“同时最多 N 个任务进入临界区”,防止下游资源(数据库连接数、API 频率限制)被打爆。并发数太高会导致下游拒绝服务、超时重试风暴,整体吞吐反而下降。合适的并发数要通过压测确定(如数据库连接池上限、目标 API 的 QPS 限制),不是越大越好。
为何常见:把并发数与吞吐量划等号,忽略了系统瓶颈通常在外部资源而不是事件循环本身。
误区协程这么好,那我写个 `while True` 一直处理数据应该没问题吧?反正它是异步的。
正解大问题!协程必须定期 await 让位,否则会霸占事件循环,导致所有其他协程饿死。如果死循环里没有任何 await,整个 asyncio 程序相当于被单线程卡死。解决办法是:循环体内部加上 await asyncio.sleep(0) 主动让位,或改用 asyncio.Queue 配合 wait_for 来设计生产者-消费者模式。
为何常见:把协程和线程混淆。线程是抢占式的,系统会自动切走;协程是协作式的,不主动让位没人能打断它。
面试题
Q1. await 关键字在协程中的核心作用是什么?
- A.阻塞线程直到 IO 操作完成
- B.将 CPU 执行权交还给事件循环并挂起当前协程← 正确答案
- C.强制销毁当前协程
- D.为协程分配更高优先级
答案:B。await 是“挂起而不阻塞”,它将执行权交还事件循环,让其他任务运行。A 是 time.sleep 的同步阻塞;C 是 cancel;D 与 await 无关,优先级由调度器/队列控制。
Q2. asyncio 的调度策略属于哪种类型?
- A.抢占式调度
- B.协作式调度← 正确答案
- C.时间片轮转调度
- D.优先级反转调度
答案:B。asyncio 是协作式调度,任务必须自觉 await 让位。线程调度(A)才是抢占式的;时间片轮转(C)是操作系统的调度算法,不是 asyncio 的核心机制。
Q3. 关于 gather 和 wait 的区别,下列哪项描述正确?
- A.gather 在有任务异常时直接向上抛,wait 需要手动检查 exception← 正确答案
- B.gather 返回集合,wait 返回结果列表
- C.gather 能控制超时,wait 不能
- D.wait 能限制并发数,gather 不能
答案:A。gather 在任务异常时默认取消其他任务并抛异常;wait 不会抛异常,任务异常被包在 Task 对象里需手动检查(task.exception())。wait 不能控制超时(要用 wait_for),二者都不能直接限流(要用 Semaphore)。
Q4. 以下哪种行为会导致 asyncio 中其他协程饿死?
- A.一个协程频繁执行 await asyncio.sleep(0)
- B.一个协程内部死循环且无任何 await← 正确答案
- C.使用 Semaphore(100) 限制并发
- D.使用 wait_for 给任务设置超时
答案:B。协作式调度下,死循环且无 await 的协程会霸占事件循环,其他协程永远得不到执行。sleep(0) 是主动让位不会饿死别人;Semaphore 和 wait_for 是保护机制,不会导致饿死。
Q5. 关于 Semaphore 的作用,下列说法正确的是?
- A.限制同时进入临界区的协程数量← 正确答案
- B.加速 CPU 密集计算
- C.保护下游资源不被超高并发打爆← 正确答案
- D.提高单协程执行效率
答案:AC。Semaphore 是限流器,限制并发协程数量以保护外部资源(数据库、API)。它不会加速 CPU 计算(B)——那是多进程/多核的职责;也不会改变单协程执行速度(D)。
Q6. asyncio.run(main()) 的作用是?
- A.为 main 创建多个子线程并行执行
- B.创建事件循环并运行 main 协程直到所有任务完成← 正确答案
- C.编译 main 协程为机器码执行
- D.为 main 分配最高优先级
答案:B。asyncio.run 创建一个新的事件循环,运行传入的协程,待其完成后关闭循环。它不会创建子线程(A),协程跑在单线程事件循环内;C 和 D 与 asyncio.run 无关。
Q7. 关于 wait 的 return_when 参数,说法正确的是?
- A.FIRST_COMPLETED 表示最早指定的那个任务完成时返回
- B.FIRST_COMPLETED 表示任一任务完成时立即返回← 正确答案
- C.ALL_COMPLETED 表示等待所有任务完成后返回← 正确答案
- D.ALL_COMPLETED 时 pending 集合始终非空
答案:BC。FIRST_COMPLETED 是任一任务(不是指定任务)完成就返回(A 错误)。ALL_COMPLETED 时所有任务都完成,pending 为空(D 错误)。FIRST_COMPLETED 常用于“先到先处理”的场景,ALL_COMPLETED 则是默认等待全部。
Q8. 以下哪项最能说明 asyncio 适合 IO 密集而非 CPU 密集任务?
- A.协程支持多核并行计算
- B.IO 等待时协程让位,CPU 被调度给其他任务← 正确答案
- C.IO 操作会自动创建新线程处理
- D.await 关键字能将计算分发到多个 CPU
答案:B。asyncio 在 IO 等待时让出 CPU,让其他协程执行,提升整体吞吐。但 C 错误——IO 不一定创建线程;A、D 错误——协程是单线程单核并发,不能直接利用多核。
Q9. 优先级队列中“数字越小优先级越高”意味着什么?
- A.数字大的任务先被执行
- B.堆顶元素是优先级最小的任务,最先被调度← 正确答案
- C.所有任务随机执行
- D.数字大小与执行顺序无关
答案:B。PriorityQueue 内部用堆排序,pop 总是弹出最小值,即优先级最高的任务。这与“数字越小优先级越高”的约定一致。
Q10. 以下哪些手段可以有效避免协程饿死?
- A.在长循环中加入 await asyncio.sleep(0)← 正确答案
- B.使用 wait_for 设置任务超时← 正确答案
- C.允许任务无限运行直到自然完成
- D.对任务做公平轮转调度(如同等优先级交替执行)← 正确答案
答案:ABD。A 让 CPU 定期让位,D 保证任务交替执行,都是防饿死手段。B 超时保护能强制取消失控任务,也防止饿死。C 是错误做法——无限运行的任务会持续霸占资源,反而是饿死的根源。