你问的“async await 原理”,实际上就是 Python 给异步编程画的那套极简操作指南。别被那些教科书上层层递进的“起初、其次”给绕晕了,咱们就直接看代码在脑子里是如何跑的。 想象一下你在忙一个不会立马出结局的事儿,比如去银行取钱,可能得等到下午。

这时候你不直接就把任务扔给一个类似“跑个马拉松”的进程,而是把它拆拆好,分成几块小任务给另一个更快的进程去干。你得告诉那个小进程:“这块先跑,跑完了把个通知戳过来。” 在程序世界里,`async` 和 `await` 就是那个拆好块、留了个口子,专门用来和另一个“跑得快”的函数对话的接口。它们俩实际上是一双鞋,缺一不可。

要是只配了 `async` 没有 `await`,那就像一个人光会穿鞋,却不会步行,任务一辈子停在那儿。

要是只有 `await` 没 `async`,那就变成了修修补补的补丁,只能在局部起功能,整个大系统就是个孤岛,动不了。

故此,务必成对出现,配合使用。 这就涉及到两个关键变量:任务本身和协调者。任务被称为“协程”,它不仅包含要执行的代码,还隐含了其他代码。协程本身是不执行代码的,它负责存放那些要执行的逻辑。它要么是一个“工单”,等着有人来干活,要么是一个“工单”正在被处理。协程本身没有状态,它只是个容器。 `async` 关键字负责给协程起个身份,告诉其他代码:“嘿,这个协程是异步的,别指望它像串行任务那样乖乖等着。”而 `await` 关键字则是那个排队的人。你让协程去做点别的事,但你去不了现场,你就站在原地,等着结局,用 `await` 把这个“站岗”的动作给提上日程。 这里有个挺直观的例子。假设你要写一个复杂的爬虫,先爬页面 A,再爬页面 B,最终爬页面 C。 ```python import asyncio async def fetch_a(): print("正在爬 A...") await asyncio.sleep(0.1) 模拟网络延迟 async def fetch_b(): print("正在爬 B...") await asyncio.sleep(0.1) async def fetch_c(): print("正在爬 C...") await asyncio.sleep(0.1) async def main(): print("启动干活") 这里把三个任务放了一起,由 main 协程来调度 你可能会看到它们被“并发”执行,互不干扰 tasks = [fetch_a(), fetch_b(), fetch_c()] 一旦任务执行完毕,main 自然也要退出 await asyncio.gather(tasks) print("完事了") asyncio.run(main()) 启动整个程序 ``` 你能看到吗?`main` 函数里,任务并没有串行地一个一个跑。它们并排在链表上,与此同时启动,与此同时暂停,与此同时终止。主线程不是在这里假惺惺地等待,而是在后台挂着另一个协程 `task_main`,专门负责这个整体的调度工作。 这时候,`await` 就登场了。当 `main` 函数执行到 `await asyncio.gather(tasks)` 这一行时,它会在队列里找到这几个任务,然后按顺序一个个“抓”下来,扔给 `task_main` 去跑,然后自己又退出来去干别的。

只要 `await` 没执行完,任务就不会真正终止。

只有当 `await` 执行完毕,任务才算“干完活了”。 要是你把这个逻辑改成串行跑(比如用 `asyncio.coroutine` 要么 `asyncio.wait_for` 这种老办法),你会发现中间的等待工夫会累加。每个 `await` 块都占用了主线程的工夫。而用 `await asyncio.gather`,那个等待的锁是放在 `main` 里面的,它负责去拿这几个任务的结局,顺便把主线程“吐出来”去干别的事。 这里有个细节,`asyncio.gather` 回的是一个任务列表。你把这个列表传给协程时,它实际执行的是“收集所有任务”的工作,而不是直接执行你给的函数。你能够把任务拆解开,比如先跑 A,等 A 跑完再跑 B: ```python async def run_sequence(): 先跑第一个 await asyncio.sleep(0.1) print("A 跑完了") 然后跑第二个,这时候主线程又退出来干了别的 await asyncio.sleep(0.1) print("B 跑完了") asyncio.run(run_sequence()) ``` 要是你把 `main` 里的那个 `await asyncio.gather(tasks)` 改成单个 `await`: ```python async def run_single(): print("干完所有事吗?") 这时候 main 退出来了 print("不干了") 什么的,这里 print("干完所有事吗?") 会跑两次! 出于在 main 里,task_main 已经退出来了,print 又被 main 调用了第二次 并且 main 本身也退出来了,没有协程去执行 main 里的代码 print("不中了,main 函数也退出来了") 结局应当是: 1.获取任务 2.task_main 执行 A 3.任务 A 执行完后,main 退出 4.任务 A 还没执行完,task_main 就退出来了 5.任务 A 还没执行完,main 退出来了 6.任务 A 还没执行完,task_main 退出来了 7.任务 A 还没执行完,main 退出来了 8.任务 A 还没执行完,task_main 退出来了 9.任务 A 还没执行完,main 退出来了 10.任务 A 还没执行完,task_main 退出来了 11.任务 A 还没执行完,main 退出来了 12.任务 A 还没执行完,task_main 退出来了 ``` 你会发现,要是你用单个 `await`,任务会卡死在“获取任务”的那一刻,然后一直挂在那儿,直到所有任务都跑完。出于 `await` 把任务交给了 `task_main`,但 `main` 的 `await` 没执行完,故此 `task_main` 不会退出来。 这就是 `await` 的魔力,它是那个“暂停当前任务,去获取下一个任务”的开关。 说到 `asyncio.gather`,它也赞成“异步收集”,也就是批量任务。比方说: ```python async def main(): tasks = [fetch_a(), fetch_b(), fetch_c()] 这里会批量获取,而不是等一个再等一个 await asyncio.gather(tasks, return_exceptions=True) ``` `return_exceptions=True` 这个参数挺实用。

要是你前面有个任务跑错了,比如 `async def bad_task(): raise ValueError()`,它会把这个毛病抛出来,害得整个程序崩溃。但要是你加了 `return_exceptions=True`,就算某个任务坏了,其他任务也能跑完,毛病会被包装成一个异常对象抛出来,程序不会直接崩掉,而是告诉你:“哦,有个任务出错了,你看这个异常”。 再聊聊 `asyncio.sleep` 和 `time.sleep` 的区别。`time.sleep` 是阻塞的,它会让主线程坐在那儿,直到工夫到,直到你调用 `time.sleep` 终止。而 `asyncio.sleep` 是非阻塞的,它只是把管住权交出去,让协程去干自己的事,但主线程在后台等着,等它干完了再回来执行 `sleep`。 要是你用 `time.sleep` 做异步工作,当你遇到一个需求 `await` 的块时,线程就会卡死,再也回不来。而 `asyncio.sleep` 解决了这个难题,它让你能够在后台省事挂起线程,处理完异步任务后,主线程还会回来持续干活。 最终说说 `asyncio` 库和其他协程的区别。`asyncio` 是专门为原生 Python 写的协程库,它匹配的是“事件循环”。事件循环会按照某种顺序(一般是轮询)去执行任务。 要是你要跑其他语言的异步库,比如 `Node.js` 的 `Promise`,要么 Java 的 `CompletableFuture`,一般不能直接用 `asyncio`。你得自己写个协程,要么用 `asyncio.to_thread` 这种库,把其他语言的异步函数转成 Python 的协程。 `asyncio.to_thread` 是个不错的方式。它专门用来把 Python 里的非阻塞函数(比如 `os.system`)转成协程。别看这个函数本身是异步的,可是它跑在另一个线程里,故此不会阻塞主线程。 比如: ```python import asyncio import threading def system_call(): print("os.system 执行") await asyncio.sleep(0.1) 模拟耗时操作 print("os.system 回") async def my_async_func(): 这里调用一个异步函数 await asyncio.to_thread(system_call) asyncio.run(my_async_func()) ``` 你看,`asyncio.to_thread` 让你能混着用 Python 的协程机制和 C 扩展库的非阻塞特性。 总结一下,`async` 和 `await` 就是一套组合拳。`async` 给任务加个身份,`await` 让它能排队、能暂停、能并行。它们不是魔法,而是让代码逻辑更灵活的工具。

只要记得 `async` 务必配 `await`,并且 `await` 负责“抓任务”,`asyncio` 负责“轮询执行”,你就不用怕协程跑不完了。 只要理解了这个逻辑,你就能写出让程序真正“跑起来”的异步代码了。