理解Python协程

理解Python协程
概述协程是用户态的轻量级线程由Python(Event Loop)程序自行调度而非操作系统通过async/await语法实现非阻塞IO在单线程内当一个任务遇到IO阻塞时事件循环会立即挂起该任务切换到另一个就绪任务从而实现单核下极高的并发吞吐量事件循环Python 协程的事件循环机制Event Loop本质上就是一个高效的、永不停歇的大 while True 循环。它是整个异步编程的“大总管”和“核心引擎”负责管理任务的排队、挂起、唤醒以及与操作系统底层的交互。1. 核心组件两大队列就绪队列Ready Queue存放着所有当场就可以立刻执行的任务比如 asyncio.create_task() 创建的任务或者刚从睡眠中醒来的任务。事件循环会按照先进先出FIFO的顺序挨个弹出队列里的任务交给 Python 解释器去执行。等待队列Waiting Queue / Timers存放着所有被卡住、正在等待外部资源或时间到期的任务比如执行到 await asyncio.sleep(1) 的任务或者在等待网络网络数据返回的任务。这里的任务没有特权去消耗 CPU它们在“静止”等待。2. 动态运作流程经典的“三步流转”消费就绪队列内政处理事件循环清点“就绪队列”。只要队列里有任务它就会把控制权Python 解释器执行权交出去让这个任务登台表演如果任务顺畅执行完它就彻底死掉释放空间。如果任务中途碰到了 await比如遇到了 sleep 或网络 I/O任务就会主动交出控制权并肉身坠入等待队列同时向大总管登记它在等什么比如等 1 秒钟或者等某个网卡数据闭眼冥想底层多路复用外交管理当事件循环把“就绪队列”里的活全部干完发现队列彻底空了的时候它不会傻傻地让 CPU 空转而是会启动“闭眼冥想”机制事件循环大总管向操作系统内核OS Kernel举白旗交出物理 CPU 的控制权并调用底层操作系统内核的多路复用 API如 Linux 的 epollMac 的 kqueueWindows 的 IOCP。它会对操作系统说“我现在手里所有的人都在睡觉最近的一个闹钟在 1 秒后。我先睡了等网卡有数据了或者 1 秒钟到了你用硬件时钟中断叫醒我。”唤醒与重新排队当 1 秒钟过去操作系统的时钟硬件发生中断把深度冬眠的事件循环大总管唤醒。大总管重新夺回物理 CPU 控制权把那个到期的任务从“等待队列”里捞出来啪地一声塞进“就绪队列”的尾部。循环重新回到第一步开始新一轮的排队消费。3.为什么它能实现高性能并发绝对的单线程协同整个事件循环以及它里面跑的所有协程自始至终都只运行在同一个物理线程里。在任何一个绝对的微观瞬间有且仅有一个任务在消耗 CPU。零硬件损耗的 指针切换协程之间的切换不需要操作系统内核介入没有昂贵的物理线程上下文切换开销。它在 Python 层面只是大总管在拨动字节码的执行指针把几 KB 的协程对象在就绪和等待队列之间来回腾挪因此即使开 10,000 个协程内存和 CPU 也毫无压力。既不浪费 CPU又能准时醒来通过将任务的“等待”委托给操作系统内核多路复用事件循环做到了有活时 CPU 拉满没活时 CPU 瞬间释放给系统其他程序同时保证时间到期时能被硬中断精准唤醒。4. 总结Python 的事件循环就是一个用单线程队列管理所有异步任务利用“控制权主动交接await”实现任务轮转并在空闲时利用“操作系统底层多路复用epoll”让出物理 CPU 的完美调度机器5. 注意点time.sleep(1) ❌【禁止在 async 协程内使用】同步阻塞休眠会卡住整个事件循环休眠期间所有其他协程全部停止运行失去并发效果。await asyncio.sleep(1) ✅【协程专用休眠】遇到 await 主动让出 CPU事件循环调度其他协程执行实现并发。深入理解代码中注释是理解的关键重点看1. await 异步任务代码 走事件循环补充在队列有任务时控制权是 Python 解释器在不同协程任务之间的“轮流宠幸权”当队列为空进入冥想时控制权就升格成了 整个 Python 进程向操作系统交出物理 CPU 的“使用权”。importasyncioasyncdefsay(msg,delay):awaitasyncio.sleep(delay)print(msg)asyncdefmain2():t1asyncio.create_task(say(A,1))# 创建任务A注册到事件循环的就绪队列t2asyncio.create_task(say(B,2))# 创建任务B注册到事件循环的就绪队列# 碰到await 主函数交出控制权给事件循环# 【事件循环开始第一轮】 事件循环从就绪队列中弹出t1执行 碰到 sleep 耗时任务 将t1放入等待队列中 交由操作系统计时器进行计时 1s# 【事件循环开始第二轮】 事件循环从就绪队列中弹出t2执行 碰到 sleep 耗时任务 将t2放入等待队列中 交由操作系统计时器进行计时 2s# 由于两轮循环切换只需要几微秒操作系统中的两个计时器几乎是在同一瞬间重叠倒计时时间重叠推进# 后续循环队列为空事件循环调用底层多路复用挂起死等最近的闹钟 (这是事件循环在没有代码可跑时为了“既不浪费 CPU又能准时醒来”而使用的“闭眼冥想”机制)# 1s 之后 A 计时结束 放回到就绪队列的尾部 事件循环看到A 执行 打印 print(A)# say(A) 执行结束 主动把控制权还给了main2函数 让main2的指针继续往下执行awaitt1# 碰到await 主函数再次交出控制权给事件循环# 事件循环开始新一轮 此时 t2还在计时 还剩1s# 1s 之后 B 计时也结束了 放回到就绪队列的尾部 事件循环看到B 执行 打印 print(B)# say(B) 执行结束 主动把控制权还给了main2函数 让main2的指针继续往下执行 / 结束awaitt2# 并发总耗时 ≈ max(1,2) 2秒asyncio.run(main2())结论create_task 的本质是【广度优先】不管三七二十一先把所有任务的“第一步”都扔进就绪队列。这样事件循环在第一轮旋转的时候就能把所有的 asyncio.sleep耗时任务全部注册到操作系统的计时器里从而实现时间的物理重叠并发2. 多任务练习 多个异步任务 执行顺序importasyncioasyncdeftask1():print(任务1开始)awaitasyncio.sleep(2)print(任务1结束)asyncdeftask2():print(任务2开始)awaitasyncio.sleep(1)print(任务2结束)asyncdefmain():# 两个协程并发启动t1asyncio.create_task(task1())t2asyncio.create_task(task2())awaitt1awaitt2 asyncio.run(main())# 打印结果# 任务1开始# 任务2开始# 任务2结束# 任务1结束3. 多任务练习 异步函数多个异步任务 执行顺序 三种情况情况一异步函数 在 开始# 这种情况 执行权一直在 say(小王) 手里直到执行完成# 在 say(小王) 完成之前后面的代码都执行不了say(小明) 和 say(小红) 注册不进事件循环importasyncioasyncdefsay(msg):print(f开始{msg})awaitasyncio.sleep(1)print(f结束{msg})asyncdefmain():awaitsay(小王)t1asyncio.create_task(say(小明))t2asyncio.create_task(say(小红))awaitt1awaitt2 asyncio.run(main())# 打印结果# 开始小王# 结束小王# 开始小明# 开始小红# 结束小明# 结束小红情况二异步函数 在 结尾# 这种情况 先后执行 create_task(say(小明))、create_task(say(小红)), say(小明) 和 say(小红) 注册到事件循环的就绪队列中# 碰到 await t1 主函数交出控制权给事件循环 执行 say(小明) 和 say(小红)# 过程省略了...# 一直到 await t2 执行完之后 给出控制权 await say(小王) 才能开始执行importasyncioasyncdefsay(msg):print(f开始{msg})awaitasyncio.sleep(1)print(f结束{msg})asyncdefmain():t1asyncio.create_task(say(小明))t2asyncio.create_task(say(小红))awaitt1awaitt2awaitsay(小王)asyncio.run(main())# 打印结果# 开始小明# 开始小红# 结束小明# 结束小红# 开始小王# 结束小王情况三异步函数 在 中间# 这种情况 say(小明) 和 say(小红) 还是会先注册到事件循环的就绪队列中# 碰到 await t1 main主函数交出控制权 事件循环依次执行 t1、t2(因为t2也加到了就绪队列中) 计时器重叠推进# 当 1s 后 say(小明)计时结束 重新进入就绪队列时 因为 say(小红) 也是 1s 所以 同时 say(小红) 也重新回到了就绪队列# 小明 执行完 也就是t1执行完 交出控制权 main主函数后半段代码进入就绪队列# 此时的就绪队列 [t2后半段代码main主函数后半段代码] 注意这时候小红确实已经在就绪队列里了而且它排在主函数的前面# 下一次事件循环 会按照顺序 先执行 t2后半段代码 所以 结束小红 先之后然后是 开始小王importasyncioasyncdefsay(msg):print(f开始{msg})awaitasyncio.sleep(1)print(f结束{msg})asyncdefmain():t1asyncio.create_task(say(小明))t2asyncio.create_task(say(小红))awaitt1awaitsay(小王)awaitt2 asyncio.run(main())# 执行结果# 开始小明# 开始小红# 结束小明# 结束小红# 开始小王# 结束小王# 当 小红 耗时大于 1s 时# 此时的就绪队列就变成了 [main后半段代码]# 碰到 await asyncio.sleep(delay) 放到等待队列 交给操作系统 去计时1s# 1.1s 后 小红后半段回到就绪队列 此时的就绪队列 [小红后半段代码]# 开始小王 就会在 结束小红 之前 先执行importasyncioasyncdefsay(msg,delay):print(f开始{msg})awaitasyncio.sleep(delay)print(f结束{msg})asyncdefmain():t1asyncio.create_task(say(小明,1))t2asyncio.create_task(say(小红,1.1))awaitt1awaitsay(小王,1)awaitt2 asyncio.run(main())# 执行结果# 开始小明# 开始小红# 结束小明# 开始小王# 结束小红# 结束小王4. asyncio.gather() 函数中放creat_task封装好的任务和异步函数 区别区别在于“这个任务到底是在遇到 gather 之前就已经进入队列启动了还是在遇到 gather 的瞬间才被启动。”维度asyncio.gather(t1, t2) (传异步任务Task)asyncio.gather(say(), say()) (传 Coroutine)任务启动时机前置启动。在调用 create_task 时就已经入队起跑。现场启动。在撞上 gather 的瞬间才被隐式入队起跑。底层微观开销略低。gather 发现是 Task直接拿来用不重复包装。略高。gather 必须在底层循环调用 create_task 现场包装。中途插播能力支持。可以在创建任务后、gather 收网前插入其他异步代码。不支持。任务的生命周期与 gather 这一行强绑定。任务独立控制权有。开发者可以在外部单独对 t1 进行 cancel() 或状态查询。无。任务句柄被封装在 gather 内部无法单独提取。5. await creat_task(异步函数) 和 asyncio.gather(creat_task(异步函数)) 区别终极选型建议。在实际编写代码时- 如果你只有一个后台任务需要等待直接写 await t1或者连 create_task 都不用直接 await 异步函数() 现场肉身直达是最干净、最轻量的做法没有多余的列表打包开销。- 如果你有多个任务需要同时并发推进、并且在未来的某一个节点统一收网无脑选择 await asyncio.gather(t1, t2, t3)。它是管理并发任务队列的标准“水泥墙关卡”维度await create_task(func())await asyncio.gather(create_task(func()))底层返回对象asyncio.Task 单任务实体asyncio._GatheringFuture 多任务聚合器控制权对接人主函数直接死等任务本身主函数死等聚合器大管家返回物物理形态原始的 Result必须在外层包裹一层 [Result] 列表异常控制力遇到报错只能原地爆炸无法拦截可以通过 return_exceptions 强行拦截并打包错误底层微观开销极低直接指针唤醒较高涉及回调注册、计数器及列表开辟

最新新闻

日新闻

周新闻

月新闻