想象一家繁忙的餐馆后厨:只有一位主厨,面前是不断转动的订单转盘,身后是十几口灶、蒸锅和烤箱。主厨把红烧肉倒进锅里,设定好定时器,转身切下一道菜的配菜;切到一半,肉好了,他放下刀回灶台装盘。没有哪个厨师会站在锅前干等——等菜的时间,就是切菜的时间。这就是asyncio的世界观。
在这间“异步厨房”里,事件循环是那位唯一的主厨,Task是贴在墙上的订单条,Future是订单上的状态贴纸,而await就是“菜下锅了,我先去忙别的”的那一转身。协程们在单线程内交替推进,靠的正是这种“不等菜、忙别事”的协作式调度。本篇将带你走遍这间厨房的每个角落:主厨如何调度、订单如何管理、菜如何出锅、gather/wait/as_completed这三样“上菜工具”各自适合什么场景。读完你会像看懂厨房动线一样,一眼看穿任何异步程序的执行节奏。
掌握事件循环与协程调度机制,意味着你能精确控制并发节奏、快速定位死锁与性能瓶颈,这是后端高并发服务与AI数据管线的底层内功。
可视化演示
展示事件循环IO多路复用机制、Task/Future状态机转换与gather/wait并发控制,切换模式掌握Python异步编程核心原理。
本节配有交互动效(进入视口后自动加载,约 3KB)。
事件循环运行机制与IO多路复用 事件循环基于epoll/kqueue同时监控多个文件描述符,IO就绪后回调对应协程恢复执行 每次循环select获取就绪事件,执行就绪回调,然后运行待处理定时器和回调队列 loop: select(fds) → dispatch(callbacks) → run_ready() → repeat 事件循环是asyncio核心引擎,单线程实现高并发IO
事件循环内核
主厨面前是一张不断转动的转盘,上面放着写满动作的卡片;旁边是定时器架子,挂着“3分钟后翻锅”“5分钟后出炉”的小闹钟。主厨的工作节奏只有三个动作:先看哪个闹钟响了,把对应卡片放上转盘;再从转盘取卡片执行;随后瞄一眼灶台有没有冒烟(IO事件),有就放一张“去看看那口锅”的卡片到转盘。如此循环往复,永不停歇。
这张“转盘”就是asyncio的事件循环(Event Loop),定时器架子是定时器堆(Timer Heap)。事件循环是整套异步系统的心脏,它单线程运行,却通过“就绪队列 + 定时器堆 + IO轮询”三件套管理所有协程、回调和IO事件。loop.run_until_complete()就像主厨说“开始营业”——把入口协程交给事件循环,一直转到它结束。
事件循环的每次迭代可抽象为:把到期的定时器回调转入就绪队列、依次执行就绪回调、最后通过selector(Linux下基于epoll)询问内核哪些socket已就绪。这种方式保证了等待不空转——CPU总是被有意义的回调占用。
主厨永远不会停下,除非转盘空了、闹钟架也空了。这只“单线程的心脏”以毫秒级的节奏跳动,支撑起整座异步大厦。理解这一点,你就抓住了asyncio的命脉:一切调度都围绕这“三件事”循环展开。
- 循环条件就绪队列
Q_ready或定时器堆T_timer中还有内容,事件循环就继续转下去,直到所有任务结束。 - 定时器转移把到期的定时器
T_due从定时器堆中取出,放入就绪队列等待执行,相当于“闹钟响了,把卡片放上转盘”。 - 执行就绪回调依次取出就绪队列中的每个回调并执行,回调内部可能又会往队列里放新卡片。
- 轮询IO事件通过selector询问操作系统哪些socket已就绪,有就绪事件就注册对应读/写回调。
Task与Future
每张订单条上都贴着一枚状态贴纸,从“已接单”到“配菜中”“烹饪中”再到“已完成”。服务员翻贴纸、主厨看贴纸、传菜员认贴纸——贴纸是整间厨房的信息中枢。在asyncio中,订单条是Task,状态贴纸是Future。
Future是一个异步结果的占位符——它承诺“将来某个时刻会有值”。而Task是Future的子类,在Future之上附加了“执行协程”的能力:asyncio.create_task(coro)把协程包装成Task并交给事件循环调度。可以这么记:Task = Future + 协程对象。Future负责状态与结果,Task负责推进协程。
Future的核心是状态机:初始为PENDING;Task被调度执行时变为RUNNING(普通Future没有RUNNING状态);协程跑完抛出StopIteration时,结果被存入Future并转为FINISHED;调用task.cancel()则抛CancelledError进入CANCELLED。当Future完成时,所有通过add_done_callback注册的回调会被安排执行,这正是唤醒等待者的机制。
想象传菜员扫一眼贴纸就知道该干什么:PENDING是“别催,还没做”,FINISHED是“可以上菜了”。订单条和状态贴配合默契,让整间厨房的数百道菜有条不紊地流转。
- 待定状态Task被创建但还没被事件循环执行过一次,订单条刚贴上墙,主厨还没顾上。
- 运行状态事件循环的
__step方法调用coro.send(None),把协程从上一个挂起点推进到下一个await处。 - 完成状态协程执行完毕抛出
StopIteration,__step捕获它并把返回值存入Future,task.result()就能取到最终结果。 - 取消状态
task.cancel()在协程的下一个挂起点抛CancelledError,协程若未捕获则进入CANCELLED。
协程调度策略
主厨炖上汤后绝不会站在蒸锅前发呆——他设定好闹钟,立刻转身去切另一道菜的配菜。这“不等锅开、转身去忙”的动作,就是协程调度的全部精髓。asyncio采用的是协作式调度:协程只有通过await主动让出控制权时,事件循环才能切换到其他协程。不存在“抢占”——没有操作系统级别的打断。
await是协程的挂起点,语义是:“我现在要等待某个结果,这段时间把控制权交还给事件循环,结果就绪时再唤醒我。”当被等待的Future还处于PENDING时,事件循环会注册一个唤醒回调,然后继续执行就绪队列中的其他任务。一旦Future完成(比如socket可读了、sleep时间到了),唤醒回调被触发,协程被重新加入就绪队列,事件的__step再次调用coro.send(None)让协程从挂起点恢复。
因为同一时刻只有一个协程在运行,asyncio天然避免了多线程的竞态问题——不需要加锁就能安全共享数据结构。代价是:如果某个协程写了一个死循环且从不await,整个事件循环都会被卡死,主厨会被一道菜困住,所有灶台停摆。这就是为什么asyncio代码里“不要写长时间阻塞的同步代码”。
- 运行片段协程从恢复点一直执行到下一个
await,这段CPU时间完全属于它,不可能被其他协程打断。 - 挂起语义执行到
await时,若等待的对象未完成,协程立刻挂起并让出事件循环。 - 恢复机制Future完成时,事件循环通过
Task._wakeup把协程重新放入就绪队列,等待下一次调度。 - 完成退出协程跑完最后一行代码,抛
StopIteration并把结果写入Future,整个Task生命周期结束。
回调链管理
菜出锅的一瞬间,铃铛响了,主厨立刻装盘、把盘子放到出菜口、按下呼叫铃让传菜员来取。一个动作触发下一个动作,环环相扣。事件循环里,这种“响应式联动”靠的就是回调链。
事件循环提供两个基础注册接口:call_soon(cb)把回调加入就绪队列,下一轮迭代立即执行;call_later(seconds, cb)把定时器塞进定时器堆,到点后转入就绪队列。而Future的set_result()内部则是一个回调链的起点:它先把结果存起来,然后call_soon调度所有通过add_done_callback注册的回调。对于Task来说,这个回调是Task._wakeup——它负责把协程重新放回就绪队列。
整个链条从IO事件到协程恢复,是这样串联的:selector检测到socket可读,调用内部读回调;读回调处理数据后调用future.set_result(data);set_result安排Task的唤醒回调;唤醒回调将Task加入就绪队列;最终事件循环执行coro.send(data)把数据送入协程。
这个链条的每个环节都是异步、非阻塞的。值得注意的是,事件循环真正执行回调是在当前调用栈结束之后——set_result说过“回头再叫你”,这句话的分量就是把回调排进了就绪队列,而不是立刻执行。因此绝不能在回调里做耗时操作,否则会阻塞整条链。
- IO就绪操作系统的epoll通知事件循环某个文件描述符可读或可写,
_process_events分发对应回调。 - 设置结果读回调解析出数据后调用
Future.set_result(),数据存入Future,状态变更为FINISHED。 - 安排回调
set_result内部把所有done回调打包,通过call_soon放入就绪队列,等待当前回调栈执行完毕后运行。 - 恢复协程事件循环执行
_wakeup时调用coro.send(data),协程在await处拿到数据继续运行。
gather并发收集
一桌客人点了一整套餐。主厨吩咐:“这张桌的菜全部做完,一次性端过去。”任何一道菜没完,都不能上桌——这就是asyncio.gather的行为模式。它是并发编程里最常用的“整桌上菜”指令。
asyncio.gather(*coros)接收多个协程或Future,内部自动把协程包装成Task并交给事件循环并发执行,最后通过一个外层Future聚合所有结果。调用await gather(...)时,当前协程挂起,直到所有子任务都完成。最关键的特点是:返回结果严格保持传入参数的顺序,不会因为某个Task先完成而乱了顺序。
但请记住它的脾气:任何一个子任务抛出异常,外层Future立刻被标记为异常,await处立即抛出该异常——即使其他任务还在运行(它们会继续跑完,结果被丢弃)。如果希望逐个处理异常,可以设置return_exceptions=True,把每个任务的异常也当作结果收集回来。
使用gather的直觉判断标准是:你是否需要“一把全拿”的结果。比如同时请求3个API再拼接数据,每个请求必须成功——那gather就是你的选择。它像一个认真负责的传菜领班,坚持“齐了才能端出去”的规矩。
\displaystyle F_{outer} = \text{gather}(c_1, c_2, \ldots, c_n), \qquadF_{outer}.result = [r_1, r_2, \ldots, r_n] \ \text{(按传入顺序)}- 包装协程
gather内部对每个协程调用ensure_future,相当于手动执行create_task,把它们全部送入事件循环。 - 外层Future
gather返回一个新的Future,内部监听所有子Task的完成事件。 - 顺序聚合所有Task完成后,按原始参数下标收集每个Task的结果,合成为结果列表。
- 异常传播默认
return_exceptions=False时,任一子任务异常都会立即向外层Future传播。
wait等待策略
服务员不一定要等整桌菜齐才行动。他可以说:“第一道菜好了我就先上”(FIRST_COMPLETED),也可以说“等这桌全部齐了我再一起端”(ALL_COMPLETED),或者“只要有任何客人投诉我就立刻处理”(FIRST_EXCEPTION)。asyncio.wait把这些“等法”交给了调用者。
asyncio.wait(tasks, timeout=None, return_when=ALL_COMPLETED)接收一个Task/Future对象的可迭代集合,返回一个二元组(done, pending)。done是已完成任务的集合,pending是未完成任务的集合。它和gather的关键区别:wait不会自动把协程包装成Task——你传给它的必须已经是Task或Future对象,直接传协程会报"coroutine was never awaited"的警告。
四种模式中,ALL_COMPLETED(默认)等全部完成;FIRST_COMPLETED等第一个完成就返回,此时pending里是还没完成的任务,通常需要手动取消它们以免泄漏;FIRST_EXCEPTION等第一个异常出现就返回,若所有任务都成功,则行为等同ALL_COMPLETED。timeout参数则可在超时后带着当前完成情况返回。
使用wait的直觉判断:你需要灵活的控制力。比如并发跑5个训练任务,只要有一个完成就先处理它的结果;再比如服务启动时要等待多个依赖组件,只要其中一个异常就立刻报错——这些场景wait比gather更合适。
- done集合返回时已完成的任务集合,可遍历
task.result()批量取结果。 - pending集合返回时尚未完成的任务集合,超时模式或
FIRST_COMPLETED模式下它往往非空,记得处理。 - 返回时机
return_when决定了wait在什么条件下返回,三个选项对应三种服务策略。 - 超时控制
timeout秒后强制返回,即使不满足return_when条件也带着当前结果返回。
as_completed完成即取
流水线出菜模式:哪道菜先出锅先端给客人,不等整桌,也不管点菜顺序。asyncio.as_completed就是这条流水线——它接收一组Task/Future,返回一个迭代器,哪个任务先完成,迭代器就先产出哪个。
与gather“收集完成后一次性给全”不同,as_completed是“逐份交付”。其内部实现:为每个传入的Task注册一个完成回调(done callback),哪条Task先完成,回调就把它的Future放进一个内部队列。迭代器不断从队列中取出已完成的Future,供调用方await获取结果。因此产出顺序是动态的,取决于实际完成时间,与传入顺序无关。
这种模式非常适合“边完成边消费”的场景:比如同时下载100张图片,每下载完成一张立即处理(压缩、上屏、入库),而不是等100张全部下载完才统一处理。内存占用更低,管道吞吐更平滑。
把as_completed用在“结果即到即用”的流程中,能显著降低端到端延迟。打个比方:gather是等100道菜齐了才开席,as_completed是做好一道上一道,客人第一道菜和最后一道菜的等待体验天差地别。选对工具,你的异步厨房才能高效出餐。
- 注册回调为每个传入的任务调用
add_done_callback,相当于在每个灶台上挂了“出锅时就叫我”的铃铛。 - 完成入队某个任务完成时,其回调把Future推入内部的
_ready_queue,迭代器从这里取货。 - 逐个await迭代器每次产出一个已完成Future,
await f立即取回结果,不产生任何等待。 - 动态顺序由于入队依赖完成时序,迭代器的产出顺序天然是“先完成先出”,与任务列表顺序无关。
AI 应用场景
异步爬虫的并发采集
阶段:数据采集与预处理
解决的问题:AI训练需要海量数据,传统串行爬虫一次只请求一个URL,抓取10万篇文章要跑几天。asyncio的并发调度能在单线程内同时发起上百个网络请求,让“等待网络响应的碎片时间”全部用来发送其他请求。
具体案例:某NLP项目需要采集50万篇新闻文章。用asyncio.gather按批次并发请求,每批500个URL,配合信号量控制并发数(限制在100以内防封IP),采集速度比串行快约40倍。所有URL返回后统一解析入库,完美契合gather“整桌上菜”的语义。
LLM批量推理的自定义调度
阶段:推理服务
解决的问题:多个用户查询并发请求大模型API时,串行等待会让后到的用户等太久。用as_completed边完成边返回,不用等最慢的查询,显著降低整体首token延迟。
具体案例:智能客服系统同时接到200个用户提问,把每个提问封装成Task并发调用LLM接口,用asyncio.as_completed不断取出最先完成的响应,先到先回复。相比gather统一等待,平均响应时间降低60%,用户体验从“等最慢”变成“就有最快”。
微服务健康检查快速失败
阶段:服务监控与运维
解决的问题:AI服务依赖多个下游组件(数据库、向量库、模型服务),逐个串行检查耗时且无法快速失败。wait的FIRST_EXCEPTION模式可以在任一组件异常时立即返回,第一时间发现故障。
具体案例:GPU推理服务的健康检查脚本,同时对Redis、特征存储、模型服务三个组件发起探活请求。用asyncio.create_task创建三个探活Task,然后await asyncio.wait(tasks, return_when=asyncio.FIRST_EXCEPTION);只要一个组件异常就立刻标记服务不健康,切换流量并触发告警。
Python 演示
import asyncio async def fetch_data(name: str, delay: float) -> dict: await asyncio.sleep(delay) return {"name": name, "delay": delay} async def demo_gather(): results = await asyncio.gather( fetch_data("A", 1.0), fetch_data("B", 0.5), fetch_data("C", 0.2), ) print("[gather] 按传入顺序返回:", [r["name"] for r in results]) async def demo_wait(): tasks = [ asyncio.create_task(fetch_data("A", 1.0)), asyncio.create_task(fetch_data("B", 0.5)), asyncio.create_task(fetch_data("C", 0.2)), ] done, pending = await asyncio.wait( tasks, return_when=asyncio.FIRST_COMPLETED ) print("[wait] 已完成数:", len(done), "未完成数:", len(pending)) for t in pending: t.cancel() async def demo_as_completed(): tasks = [ asyncio.create_task(fetch_data("A", 1.0)), asyncio.create_task(fetch_data("B", 0.5)), asyncio.create_task(fetch_data("C", 0.2)), ] for coro in asyncio.as_completed(tasks): result = await coro print("[as_completed] 最先完成:", result["name"]) async def main(): await demo_gather() await demo_wait() await demo_as_completed() if __name__ == "__main__": asyncio.run(main())
async def fetch_data(name: str, delay: float) -> dict:定义协程函数,模拟一个需要delay秒才能完成的异步任务,返回任务名和耗时。await asyncio.sleep(delay)异步等待delay秒,期间协程挂起,把控制权还给事件循环,让其他Task有机会运行。asyncio.gather(fetch_data("A", 1.0), ...)直接把协程传给gather,它会自动包装成Task并发执行,并在全部完成后按传入顺序返回结果列表。asyncio.create_task(fetch_data("A", 1.0))手动将协程包装为Task并交给事件循环调度,wait和as_completed都需要事先完成这一步。await asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)等第一个Task完成后立即返回(done, pending)两个集合。for t in pending: t.cancel()取消未完成的任务,避免它们在后台空跑造成资源泄漏。for coro in asyncio.as_completed(tasks):迭代按完成时间排序的Future序列,循环体里的await coro立刻拿到结果。
常见误区
误区我看到`async def func()`,以为像普通函数一样`func()`就会执行函数体。结果代码里没有任何print输出,程序也不报错,但函数好像“没跑”。
正解func()实际上只是创建了一个协程对象,函数体一行都没有执行。这就像厨师写好一张订单条,但还没把订单交给主厨。必须await func(),或者asyncio.create_task(func()),协程体才会真正被执行。记住:协程对象是“待执行的订单”,Task是“已被主厨接单的订单”。
为何常见:普通函数的调用语义根深蒂固,人们默认“调用了就执行”,忽略了async def的特殊身份——它定义的不是函数,而是可挂起的协程生成器。
误区`gather(coro1, coro2)`能用,那`wait(coro1, coro2)`肯定也能用吧?结果直接报"coroutine was never awaited",完全摸不着头脑。
正解gather内部会自动把协程包装成Task,而wait不做任何包装——它要求传入的必须是现成的Task或Future对象。必须提前写asyncio.create_task(coro)再传给wait。想象gather是“主厨直接把菜下锅”,wait是“你自己把菜端到锅边,主厨才肯替你烧”。
为何常见:两个API名字都带“等待”含义,参数形态又相似,极易混淆。实际上wait更底层,给了调用者更多控制权,但也要求调用者自己做更多准备。
误区sleep(0)不就是睡0秒吗,那肯定瞬间完成,几乎零开销吧?
正解sleep(0)会主动挂起当前协程,把控制权交还给事件循环,让其他就绪的协程获得执行机会。代码片段:
为何常见:字面上把"sleep 0"理解为空操作,忽略了挂起即让出调度权的底层语义。在协作式调度模型里,挂起点本身就是有意义的动作。
误区asyncio能同时跑几十个任务,肯定像多线程一样让多个任务真正同时执行吧?那CPU密集型任务用它也能加速?
正解asyncio在单线程内协作式调度,任意时刻只有一个协程在运行。并发是“交替进行”,并行才是“同时进行”。CPU密集型任务(比如大矩阵运算)不会让出CPU,用asyncio反而会因为调度开销变慢。它只擅长IO密集型任务——等待网络、磁盘、定时器时的那段“空白期”才是asyncio的用武之地。
为何常见:把“并发”和“并行”两个概念混为一谈。并发是逻辑上的同时处理多事,并行是物理上的同时执行多指令。厨房里的主厨虽然同时管着十几口锅,但他的双手一次只能炒一道菜。
误区我以为`future.set_result(x)`会像普通函数调用一样,同步依次执行所有通过`add_done_callback`注册的回调,然后回调里马上能打印结果。
正解set_result只是保存结果并安排回调到事件循环的就绪队列,并不会立刻执行。只有当前协程让出、控制权回到事件循环后,这些回调才会在后续的迭代中被执行。这就像主厨说“菜好了,回头记得装盘”——他记在了转盘上,而不是当场放下锅铲去装盘。
为何常见:事件驱动模型的“回调”看起来像立即通知,但实现上为了保持单线程安全、避免递归重入,统一把回调推迟到事件循环的下一个调度周期。理解这个“推迟一拍”的机制,是读懂asyncio内部行为的关键。
面试题
Q1. 直接调用一个async def定义的函数会得到什么?
- A.自动创建Task并立即调度执行
- B.仅返回协程对象等后续执行← 正确答案
- C.将函数转为异步生成器对象
- D.同步等待执行完再返回结果
答案:B。直接调用async def函数只创建协程对象,函数体不会执行。协程对象需要await或create_task才会被事件循环调度。类似写好订单条不等于开始做菜。
Q2. await在协程中的真正作用是什么?
- A.让出控制权等待结果就绪返回← 正确答案
- B.阻塞当前线程直到拿到结果
- C.开启子任务并行执行协程体
- D.立即同步执行并返回最终结果
答案:A。await会让当前协程挂起,将控制权交还给事件循环,等被等待的Future完成后再恢复执行。它不是阻塞线程,而是“把控制权还回去,有事再叫我”。
Q3. asyncio.gather中任一子任务抛异常时,外层Future的行为是什么?
- A.外层Future立即抛出该异常← 正确答案
- B.等待全部结束后再统一抛出
- C.自动捕获异常返回给调用方
- D.忽略异常继续等待剩余任务
答案:A。默认return_exceptions=False时,第一个子任务异常会立即传播到外层Future,await处立刻抛出异常。其他任务会继续运行但结果被丢弃。设置return_exceptions=True后才能把异常当结果收集。
Q4. 关于asyncio.wait的正确用法,以下哪项正确?
- A.接收协程对象并自动包装为Task
- B.要求传入Task或Future对象集合← 正确答案
- C.线程安全且可跨事件循环使用
- D.只能搭配FIRST_COMPLETED模式使用
答案:B。wait接收Task或Future的可迭代集合,不会自动包装协程,这是它与gather最大的区别。它返回(done, pending)集合,支持三种return_when模式,且不是线程安全的。
Q5. asyncio.as_completed的内部实现机制是什么?
- A.循环轮询任务状态直到全部完成
- B.为每个Task注册完成回调入队← 正确答案
- C.创建专用线程池监控每个任务
- D.定时扫描定时器堆中所有任务
答案:B。as_completed为每个传入任务注册done_callback,完成回调把Future推入内部队列,迭代器按队列顺序产出。这就是“谁做好谁先上”的机制,依赖回调驱动而非轮询。
Q6. asyncio.create_task()的主要作用是什么?
- A.创建协程对象并立刻同步执行
- B.包装协程为Task交付事件循环← 正确答案
- C.启动新线程来执行这个协程
- D.创建新的进程运行异步代码
答案:B。create_task把协程包装成Task对象,并自动调度到事件循环中。它不创建线程或进程,仍运行在单线程事件循环里,只是让协程被“接单”开始执行。
Q7. 事件循环每次迭代会做哪些事情?
- A.把到期的定时器回调移入就绪队列← 正确答案
- B.依次执行就绪队列中的所有回调← 正确答案
- C.通过selector监测并分发IO事件← 正确答案
- D.为每个任务启动一个新的线程池
答案:ABC。事件循环每次迭代的核心是:处理到期定时器、执行就绪队列、轮询IO事件。它绝不为任务创建线程或进程——asyncio的一切都在单线程内完成。
Q8. Future.set_result()调用后的执行流程是什么?
- A.立即同步调用所有done回调函数
- B.保存结果并安排回调到循环执行← 正确答案
- C.唤醒等待该Future的操作系统线程
- D.直接删除Future上的所有监听器
答案:B。set_result保存结果,并通过call_soon把done回调安排到事件循环的就绪队列,等当前调用栈结束后才执行。这种“推迟一拍”的设计避免了递归重入,保证单线程安全。
Q9. Task的__step方法被事件循环调用时,核心做了什么?
- A.调用协程send推进并取出完成结果← 正确答案
- B.创建子任务并行执行协程的剩余
- C.在事件循环外开新线程运行协程
- D.把协程体全部编译为回调以执行
答案:A。__step调用coro.send(None)推进协程执行到下一个挂起点;若协程抛出StopIteration,则从异常里取出返回值存入Future并标记完成。它是Task驱动协程执行的心脏。
Q10. 以下哪些操作会让当前协程挂起?
- A.await一个尚未完成的Future对象← 正确答案
- B.await一个常见的asyncio.sleep(0)← 正确答案
- C.调用当前Task的cancel()取消方法
- D.调用协程对象的close()清理方法
答案:AB。await未完成的Future会挂起当前协程,await sleep(0)主动让出控制权也属于挂起。cancel()是取消任务而非挂起,close()是关闭协程,二者都不会让协程“等待后恢复”。