一、先把最核心的一句话记住
学 async 和 await,不是在学两个新单词,而是在学一种新的程序执行方式。
普通代码的思路通常是:
做到哪,等到哪。 前面没结束,后面就别动。
而异步代码的思路是:
如果这里需要等待,那就别傻等,先把执行权交出去,让别的任务先跑。 等时机到了,再回来继续。
async 和 await,就是把这种思路写进代码里的最核心语法。
所以这一章的任务只有一个:
把你第一次看到异步代码时那种陌生感打碎。 让你真正明白,这两个关键字到底在干什么。
二、普通函数和协程函数,到底差在哪
先看我们最熟悉的普通函数:
defsay_hello(): print("你好")
这个函数调用时,执行方式很直接:
say_hello()
它会立刻从上往下执行。
再看异步函数,也叫协程函数:
asyncdefsay_hello(): print("你好")
这里多了一个 async。 这个时候,它就不再是普通函数了,而是一个协程函数。
注意,协程函数和普通函数最大的区别不是写法,而是调用后的行为不一样。
你如果这样写:
asyncdefsay_hello(): print("你好")result = say_hello()print(result)
很多初学者会以为它会直接打印你好。 其实不会。
它通常只会得到一个协程对象,而不会真正执行函数体。
这点非常关键。
普通函数一调用就执行。 协程函数一调用,先得到一个等待调度的协程对象。
所以你要先建立一个新认知:
async def 定义出来的,不是“立刻执行的函数”,而是“可以被异步运行的任务描述”。
三、为什么 async def 定义的函数不会立刻执行
因为异步程序需要一个统一的调度系统。 这个系统前一章已经讲过,就是事件循环。
协程函数只是告诉程序:
这里有一段可以异步执行的逻辑。 但到底什么时候运行、怎么切换、何时恢复,不是靠直接调用决定的,而是靠事件循环来安排。
所以你可以把协程函数理解成:
先把任务挂号 真正什么时候开始看病,由系统叫号
而普通函数更像是:
来了就立刻进门处理
这个区别一定要先彻底接受。不然你后面一写异步函数,就会老觉得怎么不执行,怎么没输出,怎么怪怪的。
四、async 到底是干什么的
async 的作用,说得最直接一点,就是:
把一个函数声明成协程函数,让它具备异步执行的资格。
比如:
asyncdeftask(): print("开始执行任务")
这段代码不是说这个函数一定会自动异步地跑起来。 而是说,这个函数现在是一个异步任务,后面可以放到事件循环里去运行。
所以 async 更像是一个身份声明。
它告诉 Python:
这个函数,不是普通同步函数。 它里面可能会出现 await。 它应该按协程的方式去处理。
先记一个判断方法:
看到 def,想到普通函数。 看到 async def,想到协程函数。
别急着往复杂里想,先把这个身份区别记牢。
五、await 到底是干什么的
await 是这一章真正的主角。
如果说 async 是在声明身份,那 await 就是在执行异步等待时完成任务切换的关键动作。
你可以先把 await 理解成一句非常口语的话:
这里我要等一下,先让别的任务跑。
比如:
await asyncio.sleep(2)
它的意思不是单纯睡两秒。 更准确地说,是:
我现在这段逻辑要暂停两秒。 在这两秒里,别把整个程序卡住,你去干别的。 两秒后再回来接着执行我。
这和普通的 time.sleep(2) 完全不是一回事。
看对比最清楚。
同步阻塞写法:
import timedeftask(): print("开始") time.sleep(2) print("结束")
这里的 time.sleep(2) 会把当前线程整个卡住。 这两秒里,这个线程基本什么也干不了。
异步等待写法:
import asyncioasyncdeftask(): print("开始")await asyncio.sleep(2) print("结束")
这里的 await asyncio.sleep(2) 则表示:
这个任务先挂起两秒,去执行别的协程。 不是整个线程傻等。
所以你一定要记住:
await 不是单纯的等待。 它是可切换的等待。
六、一个最小可运行的异步程序
看一个最基础的完整例子:
import asyncioasyncdeftask(): print("任务开始")await asyncio.sleep(2) print("任务结束")asyncio.run(task())
这个程序的执行过程是这样的:
先定义协程函数 task然后通过 asyncio.run(task()) 把它交给事件循环运行 执行到 await asyncio.sleep(2) 时,这个协程会挂起 两秒后恢复执行 最后打印任务结束
这里你第一次会看到一个新入口:
asyncio.run()
它的作用可以简单理解成:
启动一个事件循环,并把协程跑起来。
入门阶段你先这样理解就够了。 以后看到最简单的异步程序,十有八九都会有它。
七、为什么 await 只能写在 async def 里面
这是语法层面非常重要的一条规则。
错误写法:
import asynciodeftask():await asyncio.sleep(1)
这会直接报错。
因为 await 只能出现在协程函数内部,也就是 async def 定义的函数里。
为什么。
因为 await 本质上是在告诉事件循环:
我这里要挂起一下,稍后恢复。
普通函数根本没有协程上下文,也没有被异步调度的资格。 你在普通函数里写 await,就像在一辆自行车上按飞机起飞按钮,系统根本不知道怎么处理。
所以规则记死:
有 await,外面基本就必须是 async def。 普通 def 里不能直接写 await。
八、await 后面到底能接什么
这也是很多人刚入门最容易糊涂的地方。
不是任何东西都能 await。await 后面通常必须跟一个可等待对象。
入门阶段你可以先把它理解成三类:
协程对象 Task 对象 Future 对象
别被名词吓到,现在你只要先记住最常见的两种使用方式就够了。
第一种,等待另一个协程:
import asyncioasyncdeftask2(): print("task2 开始")await asyncio.sleep(1) print("task2 结束")asyncdeftask1(): print("task1 开始")await task2() print("task1 结束")asyncio.run(task1())
这里 await task2() 就是在等待另一个协程执行完。
第二种,等待异步库提供的等待操作:
await asyncio.sleep(1)
这类写法非常常见。
你现在可以先建立一个感觉:
await 后面接的,不是普通值,而是一个异步过程中需要等待完成的对象。
九、协程之间是怎么串起来的
看这个例子:
import asyncioasyncdefwash_rice(): print("开始洗米")await asyncio.sleep(1) print("洗米完成")asyncdefcook_rice(): print("开始煮饭")await asyncio.sleep(2) print("煮饭完成")asyncdefmain():await wash_rice()await cook_rice()asyncio.run(main())
虽然这里用了 async 和 await,但它依然是顺序执行的。
也就是说:
先洗米 洗完后再煮饭
这说明一个特别重要的问题:
写了异步代码,不代表它自动并发。async 和 await 只是让代码具备异步能力。 至于多个任务是不是能一起推进,还要看你后面如何调度它们。
所以你要先拆开理解:
async 和 await 解决的是等待方式问题。 并发调度是下一步的事。
这也是很多人刚学异步最容易误会的地方。 以为只要写了 async 就一定飞快。其实不是。
十、为什么说 await 会让当前协程暂停,但不会把整个程序卡住
这个点你要反复体会。
看例子:
import asyncioasyncdeftask1(): print("任务1 开始")await asyncio.sleep(2) print("任务1 结束")asyncdeftask2(): print("任务2 开始")await asyncio.sleep(1) print("任务2 结束")
如果两个任务都被事件循环调度起来,那么当 task1 执行到 await asyncio.sleep(2) 时,它会先挂起。 此时事件循环不会停着发呆,而是会去看 task2 能不能继续跑。
所以你可以把 await 理解成:
暂停的是当前协程,不是整个异步世界。
这正是异步程序能高效利用等待时间的核心。
十一、time.sleep 和 asyncio.sleep 是新手最容易混淆的一组
一定单独拿出来讲。
同步阻塞版本:
import timedeftask(): print("开始") time.sleep(2) print("结束")
异步协程版本:
import asyncioasyncdeftask(): print("开始")await asyncio.sleep(2) print("结束")
它们表面很像,差别却非常大。
time.sleep(2)会真的阻塞线程。 两秒里谁都别想往下跑。
await asyncio.sleep(2)只是让当前协程先休息两秒。 事件循环可以去调度别的协程。
所以以后只要你在异步代码里还乱用 time.sleep(),程序就很可能被你自己堵住。
一句话记忆:
同步睡眠用 time.sleep()异步等待用 await asyncio.sleep()
这两个千万别混着写。
十二、一个更直观的例子:顺序 await 和真正并发,不是一回事
先看顺序等待:
import asyncioimport timeasyncdeftask(name, seconds): print(f"{name} 开始")await asyncio.sleep(seconds) print(f"{name} 结束")asyncdefmain(): start = time.time()await task("任务1", 2)await task("任务2", 2) print("耗时:", time.time() - start)asyncio.run(main())
这段代码总耗时大概还是 4 秒。 因为它本质上仍然是:
先等任务1结束 再执行任务2
所以很多人会困惑:
不是异步了吗,为什么还是慢。
答案是: 因为你只是把等待方式改成了异步,但任务组织方式依然是串行。
异步不等于自动并发。 这是入门阶段必须跨过去的一道坎。
十三、真正让多个协程一起推进,通常要借助任务调度
虽然本章重点是 async 和 await 入门,但这里最好先让你有个概念,不然后面容易断层。
看一个简单例子:
import asyncioimport timeasyncdeftask(name, seconds): print(f"{name} 开始")await asyncio.sleep(seconds) print(f"{name} 结束")asyncdefmain(): start = time.time() t1 = asyncio.create_task(task("任务1", 2)) t2 = asyncio.create_task(task("任务2", 2))await t1await t2 print("耗时:", time.time() - start)asyncio.run(main())
这里总耗时通常接近 2 秒,而不是 4 秒。
原因不是 await 变了,而是任务被先调度起来了。 也就是说,task("任务1", 2) 和 task("任务2", 2) 不再是一个等完再一个,而是都交给事件循环一起推进。
你现在不用死记 create_task(),但要先知道:
异步代码要想产生更明显的并发效果,往往还需要任务调度手段。 单纯顺序 await,只是异步风格,不一定有并发收益。
十四、await 一个协程,到底发生了什么
这个问题讲透了,你对异步就会顺很多。
比如:
await task2()
大致可以理解为:
调用 task2(),得到一个协程对象 把这个协程交给事件循环处理 当前协程暂停,等待它执行完成task2() 完成后,再恢复当前协程继续往下走
所以 await 的本质不是“神奇等待”,而是一种明确的执行控制:
先暂停自己 等另一个异步任务完成 再回来继续
这个思路跟普通函数调用差别很大。 普通函数调用时,调用方是卡在那儿直接等。 异步 await 时,调用方是挂起,事件循环趁机去做别的任务。
十五、什么时候该用 await,什么时候不该用
一个很实用的判断方法是:
只在真正的异步等待点上用 await。
比如这些通常很适合:
等待异步网络请求 等待异步数据库操作 等待定时器 等待另一个协程执行结果 等待异步文件读写 等待消息收发
但如果你写的是普通计算逻辑,比如:
result = 1 + 2 + 3
这就不需要 await。 因为它不是等待型操作。
还有一种很常见的错误,是在异步函数里写一堆普通同步阻塞代码,然后还以为自己在异步编程。比如:
import timeimport asyncioasyncdeftask(): print("开始") time.sleep(3) print("结束")
虽然这里是 async def,但里面用了 time.sleep(3),这三秒还是会把线程卡住。 所以别只看函数头是不是 async,关键还要看里面到底是不是异步等待。
十六、async 代码写出来,为什么有时看起来反而更绕
这是学异步时一个很真实的感受。
因为普通同步代码的执行路径通常是线性的。 从上到下,顺着读就行。
异步代码则会涉及:
哪个地方挂起 哪个地方恢复 谁在等谁 哪些任务是同时推进 哪些代码虽然写在前面,但不一定先结束
所以异步编程的难点,往往不是语法,而是脑子里要同时装下多个任务状态。
这也是为什么很多人学异步时会觉得:
每一行都认识,但合起来脑子发晕。
这是正常现象。 解决办法不是死背,而是反复把任务执行顺序画清楚。
你写异步代码时,心里最好经常问自己:
这里会不会阻塞 这里是不是挂起点 这里恢复后会接着执行哪一段 现在事件循环可能会切去处理谁
只要这种思维开始形成,异步就不再神秘。
十七、一个生活化案例,把 async 和 await 彻底看懂
假设你一个人同时处理三件事:
烧水 洗菜 等外卖
同步思路是:
先站着等水开 再去洗菜 再站着等外卖
异步思路则是:
把水烧上 如果现在要等,就先去洗菜 洗菜中间发现外卖还没到,也不傻站着 等通知来了,再去取外卖
用代码思维翻译:
async def表示这是一项可以中途暂停、稍后恢复的任务
await表示这里要等待,先去做别的事
所以你会发现,async 是定义任务类型。await 是在合适的位置主动让出执行权。
这就是整套异步协程最核心的直觉。
十八、写异步代码时,一个常见误区:把所有函数都 async 化
很多人刚学协程,容易走向另一个极端:
既然异步厉害,那我干脆把所有函数都写成 async def
这通常没必要。
如果一个函数只是做纯计算、简单格式化、普通字符串处理、普通数据组装,它完全可以是普通函数。
比如:
defformat_name(name):return name.strip().title()
这种函数没什么等待操作,没必要强行写成异步。
真正适合 async def 的函数,通常是:
里面要 await 别的异步操作 本身就代表一个异步流程 需要交给事件循环调度
所以别把 async 当成流行标签。 异步是为了解决具体问题,不是为了把代码全改得更花哨。
十九、异步函数的返回值怎么理解
这一点也很重要。
看这个例子:
import asyncioasyncdefadd(a, b):await asyncio.sleep(1)return a + basyncdefmain(): result = await add(3, 5) print(result)asyncio.run(main())
这里 add(3, 5) 并不会立刻返回 8。 它先返回一个协程对象。 真正得到结果,要通过 await add(3, 5)。
所以你可以这样理解:
普通函数:调用后直接拿结果 协程函数:调用后先得到任务,await 后才拿到结果
这个差别会贯穿你后面所有异步编程。
二十、异步程序的主入口,一般怎么写
初学阶段,最常见的写法就是这种:
import asyncioasyncdefmain(): print("异步程序开始")await asyncio.sleep(1) print("异步程序结束")asyncio.run(main())
这几乎可以视为异步程序的基础模板。
你可以把 main() 理解成异步世界里的主函数。 然后用 asyncio.run(main()) 启动整个程序。
以后无论你写异步请求、异步文件处理、异步任务调度,通常都会从这个结构开始扩展。
二十一、这一章最该形成的,不是记忆,而是判断力
学到这里,你要开始形成几个很关键的判断。
第一,看到 async def,要知道它不是普通函数。 第二,看到 await,要知道它不是简单停一下,而是挂起当前协程。 第三,异步代码不是自动并发,顺序 await 一样可能是串行。 第四,真正适合异步的,是等待多的任务,不是纯计算任务。 第五,异步代码里混入阻塞式操作,会把你原本的优势直接堵死。
这些判断,比你死背几条语法更重要。
二十二、本章要点整理
async 用来定义协程函数,让函数具备异步执行资格。 协程函数调用后不会立刻执行,而是先得到协程对象。await 用来等待一个可等待对象,同时把当前协程挂起。await 只能写在 async def 定义的函数里。time.sleep() 会阻塞线程,await asyncio.sleep() 是异步等待。 顺序 await 不等于并发执行。 异步程序一般通过 asyncio.run() 启动。 异步编程适合大量等待型任务,不适合纯 CPU 密集型任务。 写异步时,不要只看语法是不是 async,更要看内部是否真的没有阻塞。