Python 异步编程详解:从并发到协程
Python 异步编程 是一种处理并发任务的编程范式,它允许程序在等待某些操作(如 I/O 操作、网络请求、数据库查询)完成时,切换到执行其他任务,从而提高程序的吞吐量和响应速度。与传统的多线程/多进程并发模型不同,异步编程通常使用协程 (Coroutines) 和事件循环 (Event Loop) 来实现,避免了线程/进程切换的开销,也绕开了 Python 的全局解释器锁 (GIL) 对 CPU 密集型任务的限制(尽管异步编程主要适用于 I/O 密集型任务)。
核心思想:异步编程通过在等待 I/O 完成时“暂停”当前任务,并“切换”到其他可执行任务,从而在单线程内实现并发和最大化 I/O 利用率。
一、为什么需要异步编程?
传统的 Python 程序(同步阻塞式)在执行 I/O 操作时会阻塞整个程序,直到 I/O 完成。例如,一个 Web 服务器在处理一个耗时的网络请求时,就无法处理其他用户的请求,导致性能低下。
1.1 同步阻塞 (Synchronous Blocking)
1 | import time |
这段代码会逐个执行 URL 请求,每个请求都会阻塞程序的执行,直到响应返回。
1.2 多线程 (Multithreading)
多线程可以实现并行执行任务,但 Python 的 GIL 限制了同一时刻只有一个线程能在 CPU 上执行 Python 字节码。对于计算密集型任务,多线程并不能真正并行加速。对于 I/O 密集型任务,线程在等待 I/O 时会释放 GIL,所以多线程在 I/O 密集型场景下确实能提高并发度。然而,线程的创建和上下文切换有开销,且存在数据竞争和锁的问题。
1 | import time |
1.3 异步编程 (Asynchronous Programming)
异步编程是一种单线程并发模型。它通过在高延迟操作(如网络请求、磁盘 I/O)发生时,将 CPU 资源让给其他任务,从而在单个线程内实现高并发。当 I/O 操作完成后,程序会“恢复”之前暂停的任务。
优点:
- 高并发、高性能 (I/O 密集型):避免了线程/进程切换的开销,以及 GIL 的限制(因为 I/O 操作时 Python 代码并没有运行)。
- 资源消耗低:协程比线程/进程更轻量级。
- 代码结构清晰:
async/await语法使得异步代码看起来像同步代码,易于理解和维护。 - 避免死锁问题:由于是单线程,不存在多线程/多进程的资源竞争和死锁问题。
缺点:
- 不适合计算密集型任务:由于是单线程,无法利用多核 CPU,计算密集型任务仍会阻塞事件循环。
- 传染性 (“Async/Await is Contagious”):一旦引入异步,相关的函数和库也需要是异步的,否则同步阻塞调用会阻塞整个事件循环。
- 调试难度稍高:异步程序的错误栈跟踪可能比同步程序复杂。
二、Python 异步编程的核心概念
Python 异步编程主要由 asyncio 库提供支持,并围绕以下核心概念展开:
2.1 协程 (Coroutines)
- 定义:协程是一种用户态的轻量级线程,它允许函数在执行过程中暂停,并在稍后从暂停点恢复执行。在 Python 中,通过
async def定义的函数就是协程函数,调用它会返回一个协程对象。 - 关键字:
async def:定义一个协程函数。await:用于等待一个可等待对象 (Awaitable) 的完成。当await一个 I/O 操作时,协程会暂停执行,并将控制权交还给事件循环,从而允许事件循环去运行其他协程。
1 | async def my_coroutine(): |
2.2 可等待对象 (Awaitables)
可等待对象是可以在 await 表达式中使用的对象。主要有三种可等待对象:
* 协程 (Coroutines):通过 async def 定义的函数被调用后返回的对象。
* 任务 (Tasks):asyncio.Task 对象,用于在事件循环中调度和运行协程。
* Future (未来对象):asyncio.Future 对象,表示一个尚未完成的操作的结果,可以被 await。
2.3 事件循环 (Event Loop)
- 定义:事件循环是异步编程的核心。它是一个无限循环,负责监听事件(如 I/O 完成、定时器到期等),并将这些事件分派给相应的协程来处理。
- 作用:当一个协程
await一个阻塞操作时,它会暂停并把控制权交还给事件循环。事件循环会去执行其他已准备好的协程。当原先的阻塞操作完成时,事件循环会通知并重新调度对应的协程继续执行。 - 获取和运行:
asyncio.get_event_loop():获取当前线程的事件循环。loop.run_until_complete(coro):运行一个协程直到它完成。asyncio.run(coro)(Python 3.7+ 推荐):一个更高级的函数,负责创建和关闭事件循环,运行协程,并处理一些细节。
graph TD
subgraph Event Loop
A[Start Event Loop] --> B{Check for ready tasks}
B -- Yes --> C["Run ready task (Coroutine)"]
C -- Await I/O --> D[Task pauses,<br>Event released]
D --> E{Wait for I/O completion / Other events}
E -- I/O completed --> F[I/O Done Event]
F --> B
C -- Task finishes --> G[Task removed]
G --> B
end
2.4 任务 (Tasks)
- 定义:
asyncio.Task是asyncio中包装协程的可等待对象。它用于在事件循环中并发地调度和运行协程。通过asyncio.create_task(coro)创建一个任务,并将其注册到事件循环中。 - 作用:如果你创建了多个任务,事件循环会在它们之间切换执行,从而实现并发。
1 | import asyncio |
运行结果可能如下 (具体时间点由调度决定):
1 | Main: Creating tasks... |
可以看到,”Task Two: Finished.” 比 “Task One: Finished.” 先输出,说明它们是并发执行的。总耗时接近最长的任务(2秒),而不是它们的总和(3秒)。
三、asyncio 模块的使用
asyncio 是 Python 用于编写并发代码的基础库,使用 async/await 语法。
3.1 基本的 async/await 示例
1 | import asyncio |
3.2 实现并发 (使用 asyncio.create_task 或 asyncio.gather)
要真正实现并发而非顺序执行,需要将协程包装成任务。
a. 使用 asyncio.create_task
1 | import asyncio |
b. 使用 asyncio.gather
asyncio.gather 是一个更方便的方式来同时运行多个可等待对象,并收集它们的结果。
1 | import asyncio |
3.3 异步网络请求示例
结合 aiohttp(一个异步 HTTP 客户端/服务器库)进行异步网络请求,相比 requests 性能更高。
1 | import asyncio |
3.4 异步迭代器和异步生成器
- 异步迭代器 (
async for):允许你在异步地获取元素时暂停。 - 异步生成器 (
async yield):允许你创建一个生成器,其生成值的过程可以是异步的。
1 | import asyncio |
四、异步编程的挑战与注意事项
同步阻塞函数会阻塞事件循环:如果在异步代码中调用了普通的同步阻塞函数,那么整个事件循环都会被阻塞,导致其他协程无法执行。
- 解决方案:对于 I/O 密集型的同步阻塞函数,可以使用
loop.run_in_executor()将其放到单独的线程池或进程池中运行,避免阻塞主事件循环。 - 对于 CPU 密集型的同步阻塞函数,也应使用
run_in_executor()放到进程池中运行,以充分利用多核 CPU 并绕过 GIL。
- 解决方案:对于 I/O 密集型的同步阻塞函数,可以使用
错误处理:异步代码中的异常处理与同步代码类似,使用
try...except块。对于asyncio.gather,如果return_exceptions=True,则异常会被作为结果返回;否则,第一个发生的异常会立即传播。取消任务 (Canceling Tasks):
task.cancel()可以尝试取消一个正在运行的任务。- 任务应该优雅地处理取消请求,通常在
try...finally块中使用asyncio.CancelledError进行捕获。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21async def cancelable_task():
try:
print("Cancelable task: Starting...")
await asyncio.sleep(5)
print("Cancelable task: Finished.")
except asyncio.CancelledError:
print("Cancelable task: Was cancelled!")
raise # 重新抛出以表明任务被取消
async def main_cancel():
task = asyncio.create_task(cancelable_task())
await asyncio.sleep(1) # 等待任务启动
task.cancel() # 取消任务
try:
await task # 等待任务真正结束 (或取消)
except asyncio.CancelledError:
print("Main: Task was indeed cancelled.")
print("Main: Done.")
if __name__ == "__main__":
asyncio.run(main_cancel())死锁和竞争条件 (避免):由于
asyncio是单线程模型,理论上不会有传统多线程的死锁问题。但如果使用asyncio.Lock等同步原语时,仍需小心编写逻辑以避免程序逻辑上的死锁(例如,一直等待一个不会被释放的锁)。
五、总结
Python 的异步编程,特别是基于 asyncio 库和 async/await 语法的协程模型,为处理 I/O 密集型任务提供了一种高效、轻量级的解决方案。
- 核心优势:通过单线程内的任务切换,实现高并发、低资源消耗,特别适用于网络服务、爬虫等场景。
- 关键概念:
- 协程 (
async def,await):可暂停和恢复的函数。 - 事件循环 (
asyncio.run,asyncio.get_event_loop):调度和管理协程执行的核心。 - 任务 (
asyncio.create_task):将协程包装成可由事件循环调度的并发单位。
- 协程 (
- 常用工具:
asyncio.gather用于并发运行多个任务并收集结果;aiohttp等第三方库提供异步 I/O 功能。 - 注意事项:避免同步阻塞代码阻塞事件循环;正确处理异常和任务取消;异步代码具有“传染性”。
随着 Python 3.7+ 对 asyncio.run() 的引入,异步编程的入门门槛已大大降低。掌握异步编程,将能极大地拓宽 Python 在高性能网络应用领域的应用范围。
