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
# 服务端处理请求(响应)
def web_request_response(*args, **kwargs):
print('=== 服务端响应 ===')
print(f'[args]: {args}')
print(f'[kwargs]: {kwargs}')
sleep(3)
# 发起请求,接收服务端响应
def web_request():
web_request_response()
# 模拟客户端发送请求
def client():
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 asyncio
from time import time
# 服务端处理请求(响应)
async def web_request_response(*args, **kwargs):
print('=== 服务端响应 ===')
print(f'[args]: {args}')
print(f'[kwargs]: {kwargs}')
await asyncio.sleep(3)
# 发起请求,接收服务端响应
async def web_request():
await web_request_response()
# 模拟客户端发送请求
async def client():
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、核心差异总结(异步优越性)
要掌握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 asyncio
import uvloop
# 设置uvloop为默认循环事件
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
1、协程的定义与执行(基础)
import asyncio
# 定义协程函数
async def worker(num: int) -> int:
print('协程对象开始执行')
await asyncio.sleep(num)
print('协程对象结束执行')
return num
➊ 方式1:asyncio.run()运行一个顶层协程,管理事件循环的生命周期,是程序的主入口。Python3.7+推荐,适用于单协程执行、脚本入口。
import asyncio
from time import time
# 定义协程函数
async def worker(num: int) -> int:
print('协程对象开始执行')
await asyncio.sleep(num)
print('协程对象结束执行')
return num
# 协程执行的第一种方式:往往被用作协程脚本的入口
def run_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 asyncio
from time import time
async def worker(num: int) -> int:
await asyncio.sleep(num)
return num
def run_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 asyncio
from time import time
# 定义协程函数
async def worker(num: int) -> int:
print('协程对象开始执行')
await asyncio.sleep(num)
print('协程对象结束执行')
return num
# 协程执行的第三种方式:create_task()方式
async def run_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:普通函数使用await
def wrong_func():
# await asyncio.sleep(1) # SyntaxError: 'await' outside async function
pass
# 错误2:await 非可等待对象
async def wrong_await():
await 123
# 错误3:未await协程对象(仅创建,不执行)
async def worker():
await asyncio.sleep(1)
async def no_await_coro():
coro = worker() # 仅创建协程对象,无调度
await asyncio.sleep(1) # 上面的协程对象未执行
下篇文章,将继续分享 asyncio 的核心API,异步上下文管理器,异步锁和异步信号量。
🔥BuildAdmin是一个永久免费开源,无需授权即可商业使用,且使用了流行技术栈快速创建商业级后台管理系统。