在现代 Python 开发中,随着应用对高并发、高性能的需求不断增加,传统同步编程方式在处理大量 I/O 操作时逐渐显得力不从心。异步编程通过极大提升程序的并发能力和资源利用率,成为了Web开发、微服务、实时通讯、数据抓取等领域的核心技术。
1. 问题场景
在开始学习 Python 的异步编程之前,先来看看传统同步编程在处理I/O 密集型任务时,存在哪些问题。I/O 密集型任务的特点是:程序在执行过程中,需要频繁等待输入/输出操作完成。例如:
- 等待网络请求返回数据
- 等待磁盘读写完成
- 等待数据库查询结果
在这些等待期间,程序会阻塞,CPU 处于空闲,无法继续做其他事情,从而大大降低了整体效率。下面通过一段代码,来模拟这个问题场景:
import time
def task1():
time.sleep(5) # 模拟一个耗时 5 秒的 I/O 操作
return 10
def task2():
time.sleep(3) # 模拟一个耗时 3 秒的 I/O 操作
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.023201704025269
可以看到,task1() 执行时,程序进入 sleep(5),必须等待 5 秒才能继续。task2() 只有在 task1() 完成后才能开始,又等待 3 秒。整体耗时大约 8 秒。
在同步编程中,一旦遇到 I/O 阻塞,整个程序会停下来等待。这导致:
- CPU 资源被浪费
- 整体执行效率低下
- 单位时间内完成的任务量少
理想情况应该是:
当 task1 处于等待时,CPU 可以切换去执行其他任务(比如 task2),而不是傻等着。这样,程序可以同时推进多个任务,从而显著提升效率。这就是异步编程要解决的核心问题。
2. 解决方法
我们希望当程序遇到 I/O 等待时,能够挂起当前任务,并切换执行其他任务。并且在合适的时机,恢复挂起的任务,继续完成后续操作。
虽然这听起来很简单,但要正确地管理任务的挂起和恢复时机,需要一套专门的调度机制。幸运的是,Python 内置的 asyncio 模块,正好为我们提供了这样的能力 —— 事件循环(Event Loop)。
它的工作流程大致是这样的:
- 首先,创建一个事件循环
- 然后,将需要执行的任务注册到事件循环中
- 最后,启动事件循环,开始调度和执行各个任务
需要注意的是,为了让事件循环正确地识别和调度任务,我们需要遵循以下两个规则:
- 任务函数定义时,需要使用
async def定义,表示这是一个异步函数,是需要交给事件循环管理的 - 在任务函数内部,当遇到需要等待其他异步操作的地方,需要使用
await。这就告诉事件循环:此处可以挂起当前任务,等异步操作完成后再恢复执行。这样可以避免程序在等待期间白白占用资源,从而提升整体效率。
通过这套机制,我们可以让程序在遇到 I/O 等待时不会空耗资源,从而大幅提升整体的执行效率。
import asyncio
import time
async def task1():
print('task1 开始执行')
# await 后面的对象必须是 async def 定义的对象,而 sleep 则不是,所以会报错
# await time.sleep(5)
# 需要切换为 async def 版本的 sleep 函数
await asyncio.sleep(5)
print('task1 结束执行')
return 10
async def task2():
print('task2 开始执行')
# await time.sleep(5)
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())
# 等待事件循环调度执行、获得 t1 任务的结果
result = await t1
print('任务1执行结果:', result)
# 等待事件循环调度执行、获得 t2 任务的结果
result = await t2
print('任务2执行结果:', result)
print('main 结束执行')
if __name__ == '__main__':
start = time.time()
# 创建事件循环
event_loop = asyncio.get_event_loop()
# 启动事件循环
event_loop.run_until_complete(main())
print(time.time() - start)
main 开始执行 task1 开始执行 task2 开始执行 task2 结束执行 task1 结束执行 任务1执行结果: 10 任务2执行结果: 20 main 结束执行 5.013811826705933
通过使用异步方式改造代码,我们发现两个任务的执行时间由原来的 8 秒缩减到 5 秒,提高了任务的执行效率。另外,在事件循环初始化这部分代码稍显复杂,我们可以使用更简洁的代码,如下所示:
# 任务执行入口点
async def main():
# 1. 获得事件循环
# 2. 注册任务函数
# 3. 等待所有任务执行结束
results = await asyncio.gather(task1(), task2())
print(results)
if __name__ == '__main__':
start = time.time()
# 创建事件循环,并启动任务执行
asyncio.run(main())
# 大概需要 5 秒
print(time.time() - start)
3. 细节探讨
上个例子中,我们提到 async 和 await 关键字,下面我们再详细了解下这两个关键字相关内容。
3.1 async
在 Python 中,async 关键字主要用于定义异步函数/协程函数,这种函数可以执行非阻塞操作(通过 await 实现),并允许在等待某些任务完成时,程序继续执行其他任务。使用 async 定义异步函数/协程函数如下代码所示:
import asyncio
async def task():
print('task')
def demo():
coro = task()
# <class 'coroutine'>
print(type(coro))
if __name__ == '__main__':
demo()
关于这段代码,有以下几个重要点需要理解:
- 协程函数的返回值不是直接的结果,而是一个 协程对象。
- 协程对象 是是一个未开始执行的任务。要执行这个任务,必须通过事件循环来调度。
import asyncio
async def task():
print('task')
def demo():
coro = task()
# 创建事件循环,并将 coro 协程对象注册到事件循环中,由事件循环调度运行
asyncio.run(coro)
if __name__ == '__main__':
demo()
Python 中的异步编程之所以需要使用协程而不是普通函数,主要是因为协程能够支持暂停和恢复执行,而普通函数则不能。
3.2 await
在 Python 中, await 关键字主要用在 async def 定义的协程函数中,用于暂停协程的执行,直到异步操作完成,然后继续执行后续代码。
import asyncio
import time
async def sub_task():
print('sub task')
return 100
async def task():
# 暂停当前的 task 执行
# 等待 sub_task 执行完毕
result = await sub_task()
# 再执行后续代码
print('result:', result)
def demo():
coro = task()
asyncio.run(coro)
if __name__ == '__main__':
demo()
await 关键字后面可以跟 coroutine、future 对象,如果 await 的是其他类型的对象可能会出现:
TypeError: xxx can't be used in 'await' expression
当 await 后面是协程对象时,事件循环暂停当前协程执行,然后继续执行 await 后面的协程对象中的代码, 此时是不会从 task1 切换到 task2。
import asyncio
async def task02():
print('task02')
return 200
async def sub_task():
print('sub_task')
return 100
async def task01():
print('task01')
result = await sub_task()
return result
async def start():
# 在事件循环中注册两个任务 task01 和 task02
# 并等待两个任务的执行结果
result = await asyncio.gather(task01(), task02())
print(result)
def demo():
asyncio.run(start())
if __name__ == '__main__':
demo()
task01 sub_task task02 [100, 200]
但是,当 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')
# 注意下面的代码:sleep 内部会执行 await future,此时任务会切换到任务2去执行
await asyncio.sleep(3)
result = await sub_task()
return result
async def start():
result = await asyncio.gather(task01(), task02())
print(result)
def demo():
asyncio.run(start())
if __name__ == '__main__':
demo()
task01 task02 sub_task [100, 200]
从执行结果来看,await future 时,事件循环确实从 task01 这个任务线切换到了 task02 这个任务线上。
3.3 future
Future 对象是异步编程中的一个重要概念,它的具体作用如下图所示:

假设:我们需要 Python 事件循环来管理两个任务。执行过程如下:
- 当事件循环执行到 task1 -> sub_task -> 网络接收数据 时,需要等待,为了不阻塞事件循环,会创建一个新的线程,并把该任务扔到该线程中去执行。
- 创建一个 future 对象,让事件循环和新创建的线程对象持有。当新线程接收到数据后,通过 future 对象将数据传递到 task1 任务对象中。当事件循环检测到 future 对象有返回结果,继续调度 task1 任务执行。
- 由于 task1 任务已经被挂起,事件循环就可以切换到任务2去执行,避免在任务1上等待。
从这里我们可以看到 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]
4. 最后小结
异步编程是一种在单线程中实现并发执行的技术,它通过“事件循环”来调度任务,遇到会等待的操作时(比如网络请求或文件读写),就把控制权交给其他任务,自己等结果再回来继续处理。
这样就避免了“等着发呆”的情况,大大提升了运行效率。与多线程不同,异步不靠开启多个线程,而是通过任务切换来充分利用时间资源。
通俗来说:异步编程就是让单线程也能干多件事,不等白等,高效非阻塞。
当程序主要瓶颈是文件读写、网络请求、数据库操作等 IO 操作时,异步编程可以在等待 IO 响应期间处理其他任务,最大化资源利用率。相比开多线程,异步在性能和资源占用上更轻量。简言之,Python 异步编程在IO密集型任务上最优,避免线程阻塞等待。
写代码时需要具备“异步思维”——凡是会等待的操作,都应该考虑是否使用 await、是否用异步库实现。每写一个任务,都应自问:这个地方是不是可以不阻塞?简言之,关注是否等待,主动识别可异步处理的环节。


冀公网安备13050302001966号