Python异步编程入门:从“傻等“到“高效“的实战指南
文章目录
前言
你的Python程序还在"站着等饭"吗?当网络请求、文件读写、数据库查询卡住时,CPU只能空转发呆。本文从同步代码的8秒困境讲起,带你走进asyncio的世界,用协程和事件循环让单线程也能并发处理多件事,性能提升立竿见影。
一、为什么需要异步编程
1.1、问题场景描述
在现在Python开发、程序在执行过程中,经常需要等待一些操作完成。这些操作包括:等待网络请求返回数据、等待磁盘读写完成、等待数据库查询结果。这些操作称为I/O操作。
在传统的同步编程中,程序遇到等待时,会停下来干等。CPU在这段时间里什么也不做,资源就浪费了。这就像你去餐厅点餐,点完餐后站在厨房门口等着,别的什么事也不做。
1.2、场景代码演示
下面这段代码展示了同步编程的问题:
import time
def task1():
# time.sleep模拟一个耗时5秒的I/O操作
time.sleep(5)
return 10
def task2():
# 模拟一个耗时3秒的I/O操作
time.sleep(3)
return 20
def main():
result = task1()
print('任务1执行结果:', result)
result = task2()
print('任务2执行结果:', result)
if __name__ == '__main__':
start = time.time()
main()
print('总耗时:', time.time() - start)
运行结果如下:
任务1执行结果: 10
任务2执行结果: 20
总耗时: 8.001485109329224
代码执行过程:
if __name__ == '__main__':首先执行这段代码,运行到start = time.time()这行代码时开始计时,接着进入到main()函数中,result = task1()调用执行task1函数,task1执行时,程序等待5秒,返回进入到main()函数result = task2()调用执行task2函数,task2必须等待task1完成后才能开始,又等待3秒- 两个任务总共花了8秒
- CPU在这8秒里大部分时间都在空等
理想情况应该是:
task1在等待时,CPU可以去执行task2。这样两个任务可以同时进行,总时间应该接近5秒(取两个任务中较长的那个)。
二、解决方法:异步编程
2.1、异步编程思想
异步编程的核心思想是:程序遇到等待时,不傻等,而是去做别的事情。等原来的那个操作完成了,再回来继续处理。
要实现这个效果,需要一套调度机制来管理任务。Python提供了asyncio模块,它的核心是事件循环。
事件循环的工作流程:
- 创建一个事件循环
- 把需要执行的任务注册到事件循环里
- 启动事件循环,开始调度任务
使用异步编程需要遵守两个规则:
- 任务函数要用
async def定义,表示这是异步函数 - 函数内部遇到需要等待的地方,要用
await标记
2.2、异步编程代码编写
import asyncio
import time
async def task1():
print('task1 开始执行')
# 注意:这里不能用time.sleep,因为time.sleep不是异步函数
# 必须用asyncio.sleep,它是异步版本的sleep
await asyncio.sleep(5)
print('task1 结束执行')
return 10
async def task2():
print('task2 开始执行')
await asyncio.sleep(3)
print('task2 结束执行')
return 20
async def main():
print('main 开始执行')
# 获取当前正在运行的事件循环
event_loop = asyncio.get_running_loop()
# 手动创建任务,注册到事件循环
t1 = event_loop.create_task(task1())
t2 = event_loop.create_task(task2())
# 等待任务1完成,获取结果
result = await t1
print('任务1执行结果:', result)
# 等待任务2完成,获取结果
result = await t2
print('任务2执行结果:', result)
print('main 结束执行')
if __name__ == '__main__':
start = time.time()
# 创建事件循环
event_loop = asyncio.get_event_loop()
# 启动事件循环,运行main函数
event_loop.run_until_complete(main())
print('总耗时:', time.time() - start)
运行结果:
main 开始执行
task1 开始执行
task2 开始执行
task2 结束执行
task1 结束执行
任务1执行结果: 10
任务2执行结果: 20
main 结束执行
总耗时: 4.998461723327637
结果分析:
- 哪一行代码写了
await关键字就表示告诉事件循环此行代码可以挂起,CPU可以去执行其他的代码 - 两个任务同时开始执行
- task2先完成(3秒),task1后完成(5秒)
- 总耗时约5秒,比同步方式的8秒快了很多
2.3、代码优化
上面的代码写起来有点复杂,需要手动创建事件循环和任务。其实有更简洁的写法:
import asyncio
import time
async def task1():
print('task1 开始执行')
# 注意:这里不能用time.sleep,因为time.sleep不是异步函数
# 必须用asyncio.sleep,它是异步版本的sleep
await asyncio.sleep(5)
print('task1 结束执行')
return 10
async def task2():
print('task2 开始执行')
await asyncio.sleep(3)
print('task2 结束执行')
return 20
async def main():
print('main 开始执行')
# asyncio.gather可以同时运行多个任务
# 它会等待所有任务完成,返回一个结果列表
results = await asyncio.gather(task1(), task2())
# 等待任务1完成,获取结果
print('任务1执行结果:', results[0])
# 等待任务2完成,获取结果
print('任务2执行结果:', results[1])
print('main 结束执行')
if __name__ == '__main__':
start = time.time()
# asyncio.run会自动创建事件循环并运行任务
# 这是最推荐的写法
asyncio.run(main())
print('总耗时:', time.time() - start)
运行结果:
main 开始执行
task1 开始执行
task2 开始执行
task2 结束执行
task1 结束执行
任务1执行结果: 10
任务2执行结果: 20
main 结束执行
总耗时: 5.0182108879089355
代码优化点:
- 用
asyncio.gather()代替手动创建任务,代码更简洁 - 用
asyncio.run()代替手动创建事件循环,一行搞定 - 这种方式是官方推荐的标准写法
三、async和await关键字详解
3.1 async关键字
async用来定义异步函数,也叫协程函数。在调用async函数时,不会立即执行,而是返回一个协程对象。
import asyncio
async def task():
print('task')
def demo():
# 调用async函数,不会立即执行,而是返回一个协程对象
coro = task()
# 打印类型,结果是<class 'coroutine'>
# coroutine类型代表的是协程对象
# 这说明task函数是一个异步函数,返回的是协程对象
print(type(coro))
if __name__ == '__main__':
demo()
运行结果:
<class 'coroutine'>
关于这段代码,有以下几个重要点需要理解:
- 协程函数的返回值不是直接的结果,而是一个 协程对象。
- 协程对象 是一个未开始执行的任务。要执行这个任务,必须通过事件循环来调度。
import asyncio
async def task():
print('task')
def demo():
coro = task()
# 创建事件循环,并将 coro 协程对象注册到事件循环中,由事件循环调度运行
asyncio.run(coro)
if __name__ == '__main__':
demo()
为什么用协程而不是普通函数
普通函数一旦开始执行,就会一直运行到结束,中间不能暂停。协程可以暂停,等需要等待的操作完成后,再恢复执行,也就是说协程能够支持暂停和恢复执行,这就是异步编程的基础。
3.2 await关键字
在Python中,await关键字主要用在async def 定义的协程函数中,用于暂停协程的执行,直到异步操作完成,然后继续执行后续代码,表示“等待某个操作完成”。
import asyncio
async def sub_task():
print('sub task')
return 100
async def task():
# await会暂停当前task的执行
# 等待sub_task执行完毕
result = await sub_task()
# sub_task完成后,继续执行后面的代码
print('result:', result)
def demo():
coro = task()
asyncio.run(coro)
if __name__ == '__main__':
demo()
await的使用规则:
await后面只能跟协程对象或future对象- 如果跟了其中其他类型的对象,会报错:TypeError: xxx can’t be used in ‘await’ expression`
await协程对象时的行为:
当 await 后面是协程对象时,事件循环暂停当前协程执行,然后继续执行 await 后面的协程对象中的代码
import asyncio
async def task02():
print('task02')
return 200
async def sub_task():
print('sub_task')
return 100
async def task01():
print('task01')
# await后面是协程对象,事件循环会继续执行sub_task
# 但不会切换到其他任务线
result = await sub_task()
return result
async def start():
# asyncio.gather同时注册多个任务
result = await asyncio.gather(task01(), task02())
print(result)
if __name__ == '__main__':
asyncio.run(start())
task01
sub_task
task02
[100, 200]
注意:这里task01先执行,然后执行sub_task,然后才执行task02。说明await协程对象时,事件循环不会切换到其他任务。
await future对象时的行为:
但是,当 await 后面的对象换成了 future 对象,则事件循环会挂起当前任务,并转到其他的任务去执行。我们也可以理解为,await future 时,就是事件循环切换任务的一个时机。
import asyncio
async def task02():
print('task02')
return 200
async def sub_task():
print('sub_task')
return 100
async def task01():
print('task01')
# asyncio.sleep内部会创建future对象
# await future时,事件循环会挂起当前任务,切换到其他任务
await asyncio.sleep(3)
result = await sub_task()
return result
async def start():
result = await asyncio.gather(task01(), task02())
print(result)
if __name__ == '__main__':
asyncio.run(start())
task01
task02
sub_task
[100, 200]
关键区别:
- await协程对象:事件循环继续执行该协程,不切换任务
- await future对象:事件循环挂起当前任务,切换到其他任务执行
asyncio.sleep()内部就是创建了future对象,所以它能触发任务切换。
四、future
future对象是事件循环和异步任务之间的桥梁。
future的作用场景:
假设程序需要通过网络获取数据。事件循环执行到这个步骤时,需要等待网络响应。为了不阻塞,它会:
- 创建一个新线程去执行网络请求
- 创建一个future对象,让事件循环和新线程都持有它
- 事件循环挂起当前任务,去执行其他任务
- 新线程拿到数据后,通过future对象把结果传回来
- 事件循环发现future有结果了,恢复原来的任务
future的作用就是帮程序获取未来某个时刻的操作结果。
import asyncio
import time
from concurrent.futures import ThreadPoolExecutor
def thread_task(futrue):
time.sleep(5)
futrue.set_result(100)
async def sub_task():
print('sub task 开始')
# 创建 future 对象
event_loop = asyncio.get_running_loop()
future = event_loop.create_future()
# 创建线程池对象
executor = ThreadPoolExecutor()
# 在其他线程执行任务
event_loop.run_in_executor(executor, thread_task, future)
# 挂起当前任务,事件循环调度其他任务执行
result = await future
print('sub task 结束')
return result
async def task1():
print('task1 开始')
result = await sub_task()
print('task1 结束')
return result
async def task2():
print('task2 开始')
await asyncio.sleep(1)
print('task2 结束')
return 200
async def main():
result = await asyncio.gather(task1(), task2())
print(result)
if __name__ == '__main__':
asyncio.run(main())
运行结果:
task1 开始
sub task 开始
task2 开始
task2 结束
sub task 结束
task1 结束
[100, 200]
结果分析:
- task1开始执行,遇到网络等待
- 事件循环创建线程去处理网络请求,同时挂起task1
- 事件循环切换到task2执行
- task2很快完成(1秒)
- 5秒后,网络请求完成,future有了结果
- 事件循环恢复task1,继续执行
五、小结
异步编程是一种在单线程里实现并发执行的技术。它通过事件循环来调度任务,遇到需要等待的操作时,就把控制权交给其他任务,等结果回来后再继续处理。这样就避免了"干等着"的情况,大大提升了运行效率。与多线程不同,异步不依靠开启多个线程,而是通过任务切换来充分利用时间。
通俗地说:异步编程就是让单线程也能同时干多件事,不用傻等,高效且不阻塞。
当程序的主要瓶颈是文件读写、网络请求、数据库操作等I/O操作时,异步编程可以在等待期间处理其他任务,最大化资源利用率。相比多线程,异步更轻量,性能更好。
结语
异步思维是一种编程习惯的升级。当你学会在await处放手、在事件循环中调度,你就掌握了让程序"不等白等"的秘诀。这不仅是技术,更是一种资源利用的智慧。把async和await用熟,你的Python代码会跑得更快、更轻、更优雅。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐
所有评论(0)