想象你是一家繁忙咖啡店的唯一咖啡师。手工滴滤咖啡需要 3 分钟“等水慢慢流”,意式浓缩只要 30 秒,而打奶泡又要 1 分钟。如果按顺序做:先做滴滤,盯着水流发呆 3 分钟,再做浓缩……那队伍早排到门外了。聪明的咖啡师会这样做:先启动滴滤(等待水流),转身去打奶泡,再回来萃取浓缩——一个人、一双手,却同时“进行”着三杯咖啡。这就是单核也能高并发的秘密:当一件事需要等待时,不傻等,而是切换去做另一件事。
asyncio 就是 Python 世界里的“咖啡师工作法”。它让你的程序在单线程内,通过不断切换任务,把等待网络、磁盘的时间利用起来,从而同时“服务”成百上千个请求。你不需要开一堆线程,也不需要担心锁竞争,只需要学会一种全新的协作方式:async/await。
对于 AI 从业者来说,无论是爬取海量训练数据、搭建模型服务网关,还是处理并发推理请求,asyncio 都是让单机吞吐量翻数倍的核心技能。它决定了你的服务能否从容面对高并发流量。
可视化演示
本节配有交互动效(进入视口后自动加载,约 3KB)。
事件循环协程调度演示 事件循环维护就绪队列,协程遇到 await 时挂起并切换到下一个就绪任务 IO 等待期间自动切换其他协程执行,单线程实现高并发无需多线程锁 切换开销仅微秒级,同一时刻只有一个协程在执行,避免竞态条件 await 点切换实现单线程高并发,IO 等待不浪费 CPU
协程核心概念
咖啡师为什么能“一心三用”?因为每一项工作都分成了许多个“微步骤”:启动滴滤是个瞬间动作,之后是漫长的等待;打奶泡也是,打发的一瞬间需要盯着,蒸气打完就可以放着。咖啡师把每杯咖啡拆成“需要动手”和“只需等待”两种片段,动手时全神贯注,等待时绝不傻等。这正是协程的哲学:把任务切成无数小片段,遇到等待就让出控制权。
在 Python 中,协程(coroutine)是一个可以暂停执行、保存当前状态、稍后恢复的函数。普通函数一旦调用,就一路执行到底;协程函数则可以在中途停下,把 CPU 让给别的协程,等时机合适再回来继续往下走。这种“走走停停”的特性,恰好和 I/O 操作的节奏合拍——网络请求发出后,绝大多数时间都在等服务器回应,而这段时间 CPU 是空闲的。
技术上说,协程是比线程更小的执行单元。一个线程内可以包含成百上千个协程,由事件循环负责调度它们。协程的切换是协作式的:协程自己决定何时让出,而不是像线程那样被操作系统强制抢占。这意味着没有数据竞争和锁的需求,编程模型更简单。但代价是:如果某个协程长时间占用 CPU 不放,其他协程就会饿死。
咖啡师最终明白:同时段内,他动手的总时间没变,但因为三杯咖啡的等待时间重叠了,顾客等到的“第一杯”的时间大大提前。这就是高并发的真相——不是变出更多双手,而是把等待串成并行。
- 忙碌片段之和协程真正占用 CPU 干活的时间总量,比如解析数据、计算特征。
- 取等待的最大值所有协程的 I/O 等待“同时进行”,所以等待总时长取决于最慢的那一个。
- 对比顺序执行如果逐个执行,总耗时是忙碌与等待的完全累加,远大于上式。
async/await语法
咖啡师随手在订单上画了两个符号:看到“☕“就知道要动手,看到”⏳”就知道该去干别的。Python 也用一对标点符号来完成这个约定:async 和 await。
用 async def 声明的函数会返回一个协程对象,调用时并不会立即执行函数体,而是返回一个可以交给事件循环调度的“任务单”。协程对象就像一杯“待制作的咖啡”,只有放进事件循环这个“工作台”,才会真正开始制作。而 await 是协程内部的暂停指令:遇到它,当前函数会挂起,让出执行权,直到等待的对象准备好再回来。
创建协程有三种常见方式。第一种:直接调用 async def 函数得到协程对象;第二种:用 asyncio.create_task() 将协程包装成任务,立刻加入调度;第三种:声明 async def main() 作为程序入口,用 asyncio.run(main()) 启动整个流程。其中 asyncio.run 会创建事件循环、运行 main 协程、结束后自动关闭循环——它是所有 asyncio 程序的“总开关”。
记住这个口诀:“async 定身份,await 让位置,run 来启动。” 咖啡师的菜单上,每一道饮品都清清楚楚写着哪些步骤需要等待、哪些步骤需要动手——而 await 就是那个“把订单插回排队架”的动作。
asyncio.run 相当于按下咖啡机的总电源;async def 是给函数贴上“可暂停”的标签;await 则是菜单上的“等待”按钮——按下后咖啡师转身去做别的单子。- async 声明标记函数是一个协程函数,调用它得到协程对象而非执行结果。
- await 挂起在协程内部暂停执行,把控制权交还给事件循环。
- asyncio.run 启动创建事件循环并运行入口协程,相当于总的调度开关。
事件循环机制
咖啡店的核心不是咖啡机,而是咖啡师脑中那张“订单调度表”。他不断扫视:哪些订单需要动手了?哪些还在等待?这正对应 asyncio 的事件循环(event loop)——一个永不停歇的调度器。
事件循环维护两组角色:就绪队列和等待队列。就绪队列里是“可以立即干活”的协程;等待队列里是“正在等 I/O 完成”的协程。循环每一轮做三件事:从就绪队列取出一个协程运行到下一个 await;检查等待队列中的 I/O 是否完成,完成则移入就绪队列;重复以上过程。这个循环运行在单线程中,速度极快,每秒钟可以完成成千上万次“检查-调度-切换”。
asyncio.run() 是一个简化版的“开店流程”:开店(创建事件循环)→ 接待第一位顾客(运行 main 协程)→ 不断调度直到所有任务完成 → 打烊(关闭循环)。在事件循环内部,Python 通过 selectors 模块监听文件描述符:当某个 socket 可读可写时,对应的协程就被唤醒。这意味着成百上千个网络连接可以共用一个线程,却各自“感觉”自己在独享 CPU。
想象咖啡师在高峰期:永远有活儿干,因为每杯咖啡的等待间隙都穿插着别的咖啡的“动手时间”。只要订单够多,他就没有一秒闲下来——事件循环就是这位永远不累的咖啡师。
- 忙碌时间占比事件循环真正用于执行用户代码的时间比例。
- 切换开销每次协程切换需要保存和恢复上下文,但这个开销极低(微秒级)。
- 空闲时间趋近于零只要还有协程在等待 I/O,切换就能填满空闲。
Task与Future
咖啡师接到一张包含 5 杯咖啡的大订单,他不会傻傻地一杯接一杯做,而是把订单拆成 5 张小卡片插在调度板上,随时准备穿插完成。在 asyncio 中,Task 就是这张“小卡片”——它是协程的调度单元,代表一个正在被事件循环管理的任务。
创建任务的方式是 task = asyncio.create_task(coro),协程会立刻被注册到事件循环中,开始参与调度(但不一定立即执行)。之后你可以 await task 等待它完成,也可以同时创建多个任务,让它们一起“并发”运行。Future 是更底层的概念:它像一个“收货单”,表示一个未来才会有的结果。当协程里写下 await future 时,就是在等这个结果送达。Task 是 Future 的子类——每个 Task 内部都有一个 Future,用来容纳协程的返回值或异常。
实践中用得最多的是 asyncio.gather():它接受多个协程或任务,并发地运行它们,然后聚合所有结果。gather 的顺序与传入顺序一致,这意味着你可以放心地按位置取用结果。除此之外还有 asyncio.wait()(更灵活但易用性稍差)、asyncio.as_completed()(先完成先返回)等工具。
咖啡师把 5 张小卡片插上调度板后,心里有数了:只要最慢的那杯咖啡完成,整张订单就可以交付。其余几杯再快,也不用等他——它们早就在穿插中做完了。这正是 gather 带给程序员的确定性安心感。
asyncio.gather() 并发执行多个协程,整体耗时约等于最慢的那个任务,而不是所有任务时间之和。这就是并发(concurrency)与并行(parallelism)的区别:并发是“交叉进行”,并行才是“同时进行”。- 创建任务把协程包装成 Task,立刻注册进事件循环的调度表。
- 并发聚合gather 同时运行多个任务,等待全部完成并收集结果。
- 总耗时取最大值并发等待多个 I/O,总时长取决于最慢的那个任务。
协程调度原理
为什么咖啡师切换任务时不会“洒了奶泡”?因为他总是在一个步骤完整结束后才切换,绝不会做到一半扔掉。对应到 asyncio,调度是协作式的——协程只有在 await 处才会让出 CPU,这就是“主动让出”的含义。
协程调度的完整生命周期是:Runnable(就绪)→ Running(运行)→ Waiting(等待 I/O)→ Runnable(回到就绪)。当协程执行到 await some_io() 时,它登记一个回调到事件循环,然后进入等待队列;I/O 完成后,事件循环把它移回就绪队列,等待下一轮调度。整个过程不涉及操作系统内核切换,代价极低。
协作式调度有一个重要特性:没有抢占。如果某个协程写了一个死循环且不含 await,事件循环永远无法切走它,其他协程全部饿死——这就是“协作”二字的代价:每个人都必须自觉让路。好在,只要遵守“所有阻塞调用都必须用异步版本”这条规则,这种风险基本不会出现。
这家咖啡店没有“抢单”文化:每个咖啡师做完手头那个“小步骤”,才把位置让给下一个任务。正因为每个人都会主动让位,所以不需要复杂的锁机制——合作的默契,比强制规则更高效。
- 就绪到运行事件循环从就绪队列取出协程,恢复它的执行上下文。
- 运行到等待协程遇到
await,主动挂起并注册 I/O 回调。 - 等待到就绪I/O 完成后回调触发,协程重新进入就绪队列。
aiohttp实战
理论说了一堆,咖啡师终于要迎接真正的“早高峰大客流”了。aiohttp 就是为协程而生的异步 HTTP 客户端/服务端库,它的请求方法是协程,天然适配 asyncio 的调度节奏。
用 aiohttp 写并发爬虫,核心模式是:先定义异步请求函数,再用 asyncio.gather 汇聚多个请求。每个 await session.get(url) 都会触发一次网络 I/O 等待,而事件循环会利用这段时间去发起其他请求。对比同步 requests 库的逐个等待,aiohttp 在爬取 100 个网页时常常能带来 10~50 倍的提速,而且只用一个线程、极低的内存开销。
aiohttp 的适用场景绝不限于爬虫。API 网关、消息推送服务、实时数据管道——凡是“连接数多、每个连接大部分时间在等待”的服务,都是 asyncio 的用武之地。一个单核的 aiohttp 服务可以轻松承载数千个并发连接,而同等配置的同步服务可能几十个连接就开始阻塞。值得注意的是:aiohttp 服务端同样遵循协程规则,await request.json()、await response.write() 都是挂起点。
咖啡店高峰期,咖啡师不再按“先来后到”单线制作,而是所有订单并行推进。用户感觉“好像有好几个咖啡师”,其实只是一个擅长调度的高手。aiohttp 就是让 Python 变成这位高手的秘籍。
- 并发请求数同时发出的请求数量,比如用 gather 一次创建 100 个会话。
- 平均响应时间并发等待下,总耗时近似等于最慢请求的耗时。
- 吞吐量定义单位时间能完成的请求数,是并发数与耗时的比值。
与线程的对比
店里来了个新咖啡师,他主张“多雇几个人不就解决了?”——这就是多线程方案:每个线程独立处理一个请求,看似简单直接。但线程的代价是:操作系统要参与调度。每一次线程切换,内核都要保存和恢复寄存器、更新调度队列,这笔开销在大量并发时迅速累积。而且线程共享内存,修改共享数据需要加锁,否则会出现“两个人同时改同一张订单”的混乱。
协程则完全不同:它运行在一个线程内部,切换由事件循环在用户态完成,没有内核参与。启动和切换开销比线程低一个量级以上——Python 中线程切换大约需要 1~10 微秒,而协程切换通常不到 1 微秒。此外,协程天然不需要锁,因为单线程内同一时刻只有一个协程在运行,不存在真正的并行访问。
但协程不是银弹。如果是 CPU 密集任务(大量计算、矩阵运算),协程不仅没有提速,反而因为切换开销变得更慢。此时应该用多进程(multiprocessing)或调用 NumPy 这类底层释放 GIL 的库。选型口诀:IO 密集用协程,CPU 密集用多进程,需要真并行才用多线程。
咖啡店终究明白:再多的咖啡师也受限于厨房尺寸(GIL 和单核),而协程的思路是在有限空间内做到极致调度。当客流(IO 密度)足够大时,一位调度大师远比十个普通员工高效。
- 线程切换开销涉及内核态进入退出、寄存器保存恢复,约 1-10 微秒。
- 协程切换开销纯用户态栈切换,约 0.1-1 微秒,快一个量级。
- GIL 的影响Python 全局解释器锁使得同一进程内线程无法真并行,进一步削弱多线程优势。
AI 应用场景
异步爬虫:批量抓取训练数据
阶段:数据采集(Data Collection)
解决的问题:训练 AI 模型需要海量文本、图片数据。用同步 requests 逐个下载 10,000 个 URL,假设每个耗时 0.5 秒,顺序执行要近 1.5 小时。asyncio + aiohttp 可以让这 10,000 个请求同时“排队等响应”,总耗时被压缩到最慢几个请求的量级,往往只需几分钟。
具体案例:某 NLP 团队需要抓取 50 万条新闻语料。他们用 asyncio.Semaphore(500) 控制并发度(避免被封 IP),配合 aiohttp 连接池复用 TCP 连接,最终在 2 小时内完成抓取,而同步方案预计需要 3 天。抓下来的数据直接进入数据清洗管道,为后续预训练提供语料。
AI 推理服务网关
阶段:模型部署与 Serving(推理服务)
解决的问题:AI 服务通常需要串联多个上游依赖:鉴权服务、特征存储、模型推理、日志上报。每一步都有网络 RTT。同步实现时,一次请求耗时是各依赖耗时之和;用 asyncio 并发调用相互独立的依赖(如鉴权 + 拉特征),单个请求的时延显著下降,网关在单核上就能支撑高并发。
具体案例:某推荐系统团队将模型服务的 Python 网关从多线程 Flask 迁移到 aiohttp + asyncio,在相同 2 核 4G 配置下,并发连接数从 300 提升到 5,000,P99 时延从 800ms 降到 350ms——因为等待特征服务的 200ms 被用来处理其他请求。
Python 演示
import asyncioimport aiohttp URLS = [ "https://www.example.com", "https://www.python.org", "https://www.github.com", "https://www.openai.com",] * 5 # 20 个 URL async def fetch(session, url): async with session.get(url) as resp: return url, resp.status, await resp.text() async def fetch_all(): async with aiohttp.ClientSession() as session: tasks = [asyncio.create_task(fetch(session, url)) for url in URLS] results = await asyncio.gather(*tasks) for url, status, html in results: print(f"{url} -> status={status}, len={len(html)}") async def demo_semaphore(): sem = asyncio.Semaphore(5) async with aiohttp.ClientSession() as session: async def limited(url): async with sem: return await fetch(session, url) tasks = [asyncio.create_task(limited(url)) for url in URLS] return await asyncio.gather(*tasks) async def main(): print("=== 全量并发 ===") await fetch_all() print("=== 信号量限流并发 ===") results = await demo_semaphore() for url, status, html in results: print(f"{url} -> status={status}, len={len(html)}") if __name__ == "__main__": asyncio.run(main())
Semaphore 限流控制并发度的标准写法。async def fetch(session, url)定义协程函数,返回请求的 URL、状态码和响应体。async with session.get(url) as resp异步发起 HTTP 请求,遇到网络 I/O 时自动挂起让出 CPU。await resp.text()异步读取响应体,同样是等待操作,会触发协程切换。asyncio.create_task(fetch(...))把协程包装成 Task 并注册进事件循环,立即参与调度。await asyncio.gather(*tasks)并发运行所有任务并收集结果,整体耗时约等于最慢的一个。asyncio.Semaphore(5)创建信号量,限制同时进行的协程数,防止对目标服务器压力过大。asyncio.run(main())启动事件循环并运行入口协程,所有任务结束后自动关闭循环。
常见误区
误区我用 asyncio 并发执行了 100 个任务,那这 100 个任务一定是同时在多核 CPU 上跑的,速度应该翻 100 倍。
正解asyncio 是单线程内的协作式调度,同一时刻只有一个协程在运行,绝不是并行。它的提速原理是“等待重叠”——把空闲等待时间用来跑别的任务。就像咖啡师只有一双手,但他能让 5 杯咖啡的等待时间重叠,从而更快交付每一杯。真正并行需要多进程或多线程(且受 GIL 限制)。
为何常见:理解“并发”和“并行”的区别需要一些底层知识。初学者看到关键字里带 async(异步),容易把它和“多任务同时跑”混淆。
误区我给所有函数都加上 async/await,程序肯定就变成高并发、性能爆表了。
正解asyncio 只对 IO 密集任务有效。如果任务是 CPU 密集型的——比如大量循环、矩阵乘、加密计算——协程切换只会增加开销,没有任何收益。更糟的是,如果有人在协程里调用 time.sleep(2) 这种同步阻塞函数,整个事件循环都会卡住 2 秒,所有协程全部停摆。正确做法是:CPU 密集用多进程,同步阻塞 IO 用线程池(asyncio.to_thread)。
为何常见:人们天然以为“新技术 = 更快”。asyncio 的适用边界(IO 等待主导)不是直觉能判断的,必须理解它的调度模型。
误区我定义了一个 `async def fetch_data()`,然后调用 `fetch_data()`,为什么网页没被抓到?难道不应该自动执行吗?
正解调用 async def 函数只会创建协程对象,函数体一行都不会执行。你必须让它进入事件循环才会运行——要么 await fetch_data(),要么 asyncio.create_task(fetch_data()),要么挂在 asyncio.gather 里。这就好比你把菜单递给咖啡师却一直握着不松手,咖啡当然不会开始做。
为何常见:协程的“惰性执行”与普通函数差异巨大。普通函数调用即执行,协程调用只返回对象——这个转变违反直觉,是 asyncio 最常见的“无声 bug”来源。
误区我在协程里用 `requests.get()` 代替 aiohttp 也可以吧?反正我写成 async 函数了。
正解绝对不行。requests.get() 是同步阻塞调用,在协程里调用它会独占整个线程——事件循环被卡住,所有并发任务全部暂停。这就像咖啡师在等滴滤时停下所有工作盯着水流,其他订单全被晾着。asyncio 世界要求“所有耗时操作都必须是异步版本”:HTTP 用 aiohttp、睡眠用 asyncio.sleep()、文件读写用 aiofiles 或 asyncio.to_thread()。
为何常见:很多人不知道同步库会阻塞事件循环。只要有一次 requests.get() 被塞进关键路径,整个服务的“高并发”就名存实亡。
误区以后我都用 asyncio 写服务,彻底抛弃多线程多进程。
正解asyncio 是 IO 密集场景的利器,但并非万能。CPU 密集任务用 multiprocessing,混合场景可能需要多进程 + 协程的组合;老代码依赖的同步库无法轻松改造成异步版本;调试 asyncio 的堆栈和资源释放也相对复杂。咖啡师再厉害,也需要烤箱(多进程)处理烘焙,需要柜台(GIL)管理空间。
为何常见:学完一个新技能后,人们容易过度推广它的适用范围。选型永远要回到“瓶颈是什么”来分析,而不是用某一种技术包打天下。
面试题
Q1. 关于 Python 中协程(coroutine)的描述,下列哪项正确?
- A.协程可以在多个 CPU 核心上并行运行
- B.协程可以在单线程内通过等待时切换实现高并发← 正确答案
- C.协程必须依赖操作系统内核进行调度
- D.协程之间必须使用锁来保护共享数据
答案:B。协程运行在单线程中,由事件循环在用户态完成调度,不涉及内核。它的高并发来自“等待时切换”,而非并行。选项 A 混淆了并发与并行;C 错在协程是纯用户态调度;D 错在单线程同一时刻只有一个协程运行,不需要锁。就像咖啡师一双手,靠穿插完成多杯咖啡,而不是真的同时动手。
Q2. 在 asyncio 中,await 的作用是什么?
- A.强制程序单线程执行
- B.启动事件循环
- C.挂起当前协程,等待可等待对象完成后恢复← 正确答案
- D.创建新的协程对象
答案:C。await 会暂停当前协程的执行,把控制权交还给事件循环,待等待对象(Task、Future、IO 等)完成后恢复。它不负责创建协程(那是 async def 调用和 create_task 的事),也不直接启动事件循环(那是 asyncio.run 的职责)。用咖啡店的比喻,await 就是“把订单插回排队架,转身去忙别的”。
Q3. 以下哪段代码能正确启动一个 asyncio 程序入口?
- A.
asyncio.run(main())← 正确答案 - B.
main().run() - C.
await main() - D.
async def run(main())
答案:A。asyncio.run(main()) 会创建事件循环、运行 main 协程、结束后自动关闭,是标准入口。await 必须在协程内部使用,不能直接在模块顶层执行;main().run() 不存在此方法;async def run(main()) 是语法错误。咖啡店必须按下“总电源开关”(asyncio.run)才能开始运转。
Q4. 关于 asyncio.create_task(),以下说法正确的是?
- A.它会立即执行协程并等待结果
- B.它将协程包装为 Task 并注册进事件循环,使其参与调度← 正确答案
- C.它只能被调用一次,不能重复使用
- D.它会在新线程中运行协程
答案:B。create_task 将协程包装成 Task 并注册到事件循环,协程成为调度对象,但不会立刻执行完毕,也不会阻塞调用方。重复调用 create_task 是完全合法的。它运行在同一个线程中,绝不创建新线程。就像咖啡师把新订单卡片插上调度板——具体何时做,要看整体调度安排。
Q5. 以下哪些场景适合使用 asyncio?
- A.大规模网络爬虫,大量 HTTP 请求等待响应← 正确答案
- B.高并发 API 网关,请求等待下游服务响应← 正确答案
- C.纯 CPU 密集的矩阵运算
- D.大量文件读取的 IO 密集任务← 正确答案
答案:ABD。asyncio 的核心优势在 IO 密集场景——网络请求、文件读取等“等待”占主导的任务。选项 A、B、D 都是等待占主导,协程能通过切换提升吞吐。选项 C 是 CPU 密集,协程不仅无效还会因切换开销变慢,应选择多进程。题目变体可反选“不适合用 asyncio 的场景”。
Q6. 事件循环中,“就绪队列”和“等待队列”的角色是什么?
- A.就绪队列存放已完成的协程,等待队列存放未创建的协程
- B.就绪队列存放可立即运行的协程,等待队列存放正在等 IO 的协程← 正确答案
- C.两者没有区别,是同一个队列的两种叫法
- D.就绪队列存放线程,等待队列存放协程
答案:B。事件循环每轮从就绪队列取出协程运行,同时检查等待队列的 IO 是否完成,完成则移入就绪队列。就绪=可以干活,等待=正在等待外部事件。这就好比咖啡师的“待做单”和“等咖啡机”两个区域——等机器完成的订单一旦好了,就移到待做区。
Q7. 在 async 函数中调用了同步库 requests.get(),会出现什么问题?
- A.没有任何问题,正常并发执行
- B.只在多线程中崩溃,单线程没问题
- C.会阻塞整个事件循环,所有协程暂停直到请求完成← 正确答案
- D.会启动一个新的子进程执行请求
答案:C。同步库的阻塞调用会独占线程,事件循环无法切换任务,所有协程全部被卡住。asyncio 要求所有耗时操作必须用异步版本(aiohttp、asyncio.sleep 等),或通过 to_thread 放入线程池。就像咖啡师停下来专心等一台机器,其他所有订单都停摆了——协作式调度的前提是每个人都要主动让位。
Q8. 关于 asyncio 与多线程的对比,哪些说法正确?
- A.线程切换由操作系统调度,协程切换由用户态事件循环调度← 正确答案
- B.协程切换开销通常远小于线程切换开销← 正确答案
- C.多线程一定比 asyncio 快
- D.因为 GIL,Python 多线程无法实现真并行← 正确答案
答案:ABD。A 正确,线程是内核级调度、协程是用户态调度;B 正确,协程切换通常不到 1 微秒而线程切换 1-10 微秒;D 正确,GIL 使得同一进程内多线程无法并行执行 Python 字节码。C 错误,IO 密集场景 asyncio 通常效率更高,多线程反而因切换开销而变慢。
Q9. 以下关于 asyncio.gather() 与 asyncio.create_task() 搭配使用的说法,哪个正确?
- A.必须先用 create_task 再调用 gather,否则 gather 无效
- B.gather 可以接收协程或 Task,并发运行后按传入顺序返回结果← 正确答案
- C.gather 内部会自动创建新的事件循环
- D.gather 只能接收一个协程参数
答案:B。gather 既能接收协程(会自动包装为 Task),也能直接接收 Task,并发运行后按传入顺序返回结果列表。它不需要配对 create_task,但使用 create_task 可以先拿到任务句柄做取消操作。gather 不会新建事件循环,它运行在当前循环中。咖啡师一次接多张订单卡,最后按点单顺序把饮品递给顾客。
Q10. 协作式调度下,以下哪些操作会导致其他协程被饿死?
- A.协程内使用 while True 死循环且无 await← 正确答案
- B.协程内调用
asyncio.sleep(0)的无限循环 - C.协程内执行耗时极长的同步 CPU 运算← 正确答案
- D.协程内使用
time.sleep(0.01)← 正确答案
答案:ACD。协作式调度只在你主动让出 CPU 时才切换。死循环无 await(A)和长 CPU 运算(C)都会独占 CPU 不让出;time.sleep 是同步阻塞(D),同样卡死事件循环。而 asyncio.sleep(0) 会让出控制权,事件循环得以调度其他协程,所以 B 不会饿死别人——它相当于“假装忙碌但每次都让位”,别的协程有机会执行。咖啡师若一直埋头打磨一杯咖啡不放,其他订单自然全被饿死。