❝Python入门第四十课,主要针对asyncio的核心价值IO密集型任务的处理上,从底层维度拆解同步与异步的差异,为深度理解 asyncio 核心概念奠定基础。
一、同步VS异步 深度对比(底层逻辑+性能本质)
asyncio的核心价值体现在IO密集型任务的处理上,先从底层维度拆解同步与异步的本质差异,为理解asyncio奠定基础。
1、核心维度深度对比表:
| 对比维度 | 同步编程(Sync) | 异步编程(Async,基于 asyncio) |
|---|
| 执行模型 | 串行阻塞:代码按照顺序执行,遇到IO(网络/文件/数据库)时,整个进程/线程暂停等待,直到IO完成 | 非阻塞并发:代码执行到IO时,协程挂起(放弃CPU),事件循环调度其他就绪协程执行,IO完成后恢复原协程。 |
| 底层调度 | 操作系统内核调度(线程/进程切换),切换成本高(内核态→用户态) | 用户态调度(事件循环调度协程),切换成本极低(仅函数调用级) |
| 资源占用 | 为避免阻塞,需创建多线程/多进程(如1000个请求需要1000个线程),内存占用高(每个线程栈≈1MB) | 单线程/少量线程即可处理大量并发(1000个请求仅需1个线程),内存占用极低 |
| IO处理逻辑 | 主动等待(Polling):CPU空转等待IO结果 | 被动通知(Event-driven):IO完成后由操作系统触发回调 |
| 性能表现 | IO密集型任务:总耗时≈各任务耗时之和(如5个2秒任务→10秒);CPU密集型:无劣势 | IO密集型任务:总耗时≈最长单个任务耗时(如5个2秒任务→2秒);CPU密集型:无优势(协程无法利用多核) |
| 编程思维 | | 异步思维:需关注协程挂起/恢复点,需结合await/事件循环,入门门槛高 |
| 典型实现 | requests(HTTP)、pymysql(数据库)、time.sleep() | aiohttp(HTTP)、aiomysql(数据库)、asyncio.sleep() |
2、直观性能对比:同步VS异步处理IO密集型任务
以下代码模拟「批量网络请求(IO密集型)」场景,清晰体现异步的性能优越性:
"""sync 同步实现【模拟】"""# 客户端发起请求 → 服务端接收请求 → 服务端处理请求from time import time, sleep# 服务端处理请求(响应)defweb_request_response(*args, **kwargs): print('=== 服务端响应 ===') print(f'[args]: {args}') print(f'[kwargs]: {kwargs}') sleep(3)# 发起请求,接收服务端响应defweb_request(): web_request_response()# 模拟客户端发送请求defclient(): begin_time = time()for x in range(1, 4):if x > 1: print() print(f'=== 客户端第{x}次请求 ===') web_request() spent_time = time() - begin_time print(f'\n[同步请求耗时]: {spent_time}秒')if __name__ == '__main__': client()
上面的代码运行结果是,同步请求耗时:9.001716136932373秒。
"""async 异步实现【模拟】"""# 客户端发起请求 → 服务端接收请求 → 服务端处理请求import asynciofrom time import time# 服务端处理请求(响应)asyncdefweb_request_response(*args, **kwargs): print('=== 服务端响应 ===') print(f'[args]: {args}') print(f'[kwargs]: {kwargs}')await asyncio.sleep(3)# 发起请求,接收服务端响应asyncdefweb_request():await web_request_response()# 模拟客户端发送请求asyncdefclient(): begin_time = time() tasks = []for x in range(1, 4):if x > 1: print() print(f'=== 客户端第{x}次请求 ===') tasks.append(asyncio.create_task(web_request()))for task in tasks:await task spent_time = time() - begin_time print(f'\n[异步请求耗时]: {spent_time}秒')if __name__ == '__main__': asyncio.run(client())
上面的代码运行结果是,异步请求耗时:3.0060555934906006秒。
3、核心差异总结(异步优越性)
- 耗时差异:同步版本总耗时=5×2=10秒(串行等待),异步版本总耗时≈2秒(所有任务并发执行,仅等待最长的IO耗时);
- CPU利用率:同步版本CPU空转90%以上(等待IO),异步版本CPU在IO等待期间可调度其他任务,利用率接近100%;
- 资源占用:同步版本若处理1000个请求需创建1000个线程(内存占用≈1GB),异步版本仅需1个线程(内存占用≈MB级别)。
二、asyncio 核心概念(深度理解)
要掌握asyncio,需先吃透以下核心概念,这是理解异步编程的基础:
1、协程(Coroutine):异步编程的基本单元
定义
通过async def定义的函数,调用后返回『协程对象』(而非直接执行),需通过事件循环调度执行。
核心特性
可暂停(await)、可恢复,暂停时不阻塞线程,是用户态的“轻量级线程”。
与线程的区别
线程:内核调度,切换成本高(需保存寄存器/栈帧);
协程:用户态调度(事件循环),切换成本仅为函数调用,无内核开销。
2、事件循环(Event Loop):asyncio的“心脏”
定义
asyncio的核心调度器,负责管理所有协程的生命周期。一个事件循环每次运行一个 Task 对象。
核心职责
➊ 注册/调度协程/Task;
➋ 监听IO事件(网络/文件),触发回调;
➌ 切换挂起的协程,实现非阻塞执行;
➍ 处理定时器、信号等异步任务。
底层实现
基于操作系统的IO多路复用(epoll/kqueue/select),实现“单线程监听多IO事件”。
3、可等待对象(Awaitable):await 的唯一合法操作数
await右侧必须是『可等待对象』,包含三类:
➊ 协程对象:async def函数调用的返回值;
➋ Task 对象:协程的可调度封装(asyncio.create_task()创建),事件循环的直接调度单元;
➌ Future 对象:表示“未来完成的异步操作结果”,Task 继承自 Future,底层用于封装异步IO的结果。
4、Task vs Future
概念介绍
Future:底层抽象,手动创建需调用set_result()/set_exception()标记完成,通常无需手动使用;
Task:Future 的子类,专为协程设计,create_task()自动将协程封装为Task,并交由事件循环调度,是实际开发中最常用的对象。
5、事件循环策略(Event Loop Policy)
定义
管理事件循环的创建/获取/销毁的规则,默认由asyncio提供;
进阶优化
Linux 下可替换为uvloop(基于libuv实现,性能比默认循环快2-4倍)。
安装:
pip install uvloop
使用:
import asyncioimport uvloop# 设置uvloop为默认循环事件asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
三、asyncio 基础语法(核心入门)
1、协程的定义与执行(基础)
import asyncio# 定义协程函数asyncdefworker(num: int) -> int: print('协程对象开始执行')await asyncio.sleep(num) print('协程对象结束执行')return num
➊ 方式1:asyncio.run()运行一个顶层协程,管理事件循环的生命周期,是程序的主入口。Python3.7+推荐,适用于单协程执行、脚本入口。
import asynciofrom time import time# 定义协程函数asyncdefworker(num: int) -> int: print('协程对象开始执行')await asyncio.sleep(num) print('协程对象结束执行')return num# 协程执行的第一种方式:往往被用作协程脚本的入口defrun_function_1():# asyncio.run(需要传入协程对象),每执行一次run方法就会创建一个事件循环# 所以下面的代码会创建3个时间循环 begin = time() res1 = asyncio.run(worker(3)) res2 = asyncio.run(worker(3)) res3 = asyncio.run(worker(3)) spent = time() - begin print(f'[第一种方式]:{res1}{res2}{res3},耗时:{spent}秒')if __name__ == '__main__': run_function_1()
上面的代码运行结果:
协程对象开始执行协程对象结束执行协程对象开始执行协程对象结束执行协程对象开始执行协程对象结束执行[第一种方式]:3 3 3,耗时:9.018600463867188秒
执行结果是9秒,为什么不是我们期望的3秒呢?原因是:asyncio.run()每运行一次都会创建一个新的事件循环,那么上面的代码就创建了3个事件循环,所以执行结果和同步是一样的效果。
➋ 方式2:手动管理事件循环(兼容Python3.6以下),适用于旧版本兼容、自定义事件循环配置。
由于老版本已经基本很少使用了,了解下面的语法即可:(注意:下面的代码只是展示语法,并未实际运行)
"""协程执行旧版本的写法 Python3.6以下"""import asynciofrom time import timeasyncdefworker(num: int) -> int:await asyncio.sleep(num)return numdefrun_function_2(): begin_time = time() loop = asyncio.get_event_loop()try: loop.run_until_complete(worker(5)) loop.run_until_complete(worker(5)) loop.run_until_complete(worker(5))except Exception as e:raise e spent_time = time() - begin_time print('耗时:{spent_time}' . format(spent_time))if __name__ == '__main__': run_function_2()
➌ 方式3:create_task + asyncio.run并发执行多协程,使用于生产环境(体现asyncio核心价值)。asyncio.create_task()将协程包装成一个 Task 对象,并排入事件循环等待调度。这是实现并发的主要方式。
import asynciofrom time import time# 定义协程函数asyncdefworker(num: int) -> int: print('协程对象开始执行')await asyncio.sleep(num) print('协程对象结束执行')return num# 协程执行的第三种方式:create_task()方式asyncdefrun_function_3(): begin = time()# 下面的写法,是将三个协程Task对象,封装到一个事件循环之中 task1 = asyncio.create_task(worker(1)) task2 = asyncio.create_task(worker(2)) task3 = asyncio.create_task(worker(3)) res1 = await task1 res2 = await task2 res3 = await task3 spent = time() - begin print(f'[第三种方式]:{res1}{res2}{res3},耗时:{spent}秒')if __name__ == '__main__': asyncio.run(run_function_3())
上面的代码运行结果:
协程对象开始执行协程对象开始执行协程对象开始执行协程对象结束执行协程对象结束执行协程对象结束执行[第三种方式]:1 2 3,耗时:3.0070319175720215秒
这次运行结果,就是我们期望看到的结果了。
❝总结:一般我们是把第一种和第三种结合使用,把 asyncio.run()作为协程脚本的入口,将 asyncio.create_task() 作为封装过程。
2、async/await 核心规则(重要)
async def定义的函数只能返回『协程对象』,不能直接执行;await仅能在async def函数内使用,右侧必须是可『等待对象』;- 未被
await的协程对象不会执行(仅创建,无调度); - 嵌套协程:
async def函数内可调用其他async def函数,但必须加await。
3、错误示例与修正(避坑)
import asyncio# 错误1:普通函数使用awaitdefwrong_func():# await asyncio.sleep(1) # SyntaxError: 'await' outside async functionpass# 错误2:await 非可等待对象asyncdefwrong_await():await123# 错误3:未await协程对象(仅创建,不执行)asyncdefworker():await asyncio.sleep(1)asyncdefno_await_coro(): coro = worker() # 仅创建协程对象,无调度await asyncio.sleep(1) # 上面的协程对象未执行