Python 的异步编程模型从 3.4 到 3.5 经历了一次语法层面的重大转折。这段时间我陆续用了三种写法,从回调到 yield from,再到 async/await,每种方案的设计取舍都不同。
这篇文章从「同步 I/O 为什么占线程」出发,梳理 Python 异步编程的演进路径。
同步 I/O 的瓶颈
先看一段标准的 Flask 接口代码:
from flask import Flask
import pymysql
app = Flask(__name__)
@app.route('/user/<int:user_id>')
def get_user(user_id):
conn = pymysql.connect(host='localhost', user='root', db='test')
cursor = conn.cursor()
cursor.execute('SELECT * FROM users WHERE id = %s', (user_id,))
result = cursor.fetchone()
conn.close()
return str(result)
逻辑正确,但并发能力受限于线程池大小。每个请求占用一个线程,线程在 cursor.execute() 处阻塞,等待 MySQL 返回结果。这段时间内线程不释放,也无法处理其他请求。
用 ab 压测:
$ ab -n 100 -c 10 http://localhost:5000/user/1
Requests per second: 42.31 [#/sec] (mean)
Time per request: 236.320 [ms] (mean)
42 QPS。瓶颈不在 CPU,也不在 MySQL,而在「线程被 I/O 阻塞」。
线程是稀缺资源。一个线程默认栈空间 8MB,1000 个并发连接需要约 8GB 内存。同步模型的可扩展性很差。
异步模型的核心思路是:I/O 等待时不占用线程,等 I/O 完成后再通过回调通知。
回调模型
Python 3.4 的 asyncio 支持回调风格的异步编程:
import asyncio
def query_user(user_id, callback):
fut = asyncio.Future()
def on_result(fut):
result = fut.result()
callback(result)
async_mysql.query(
'SELECT * FROM users WHERE id = %s',
(user_id,),
fut
)
fut.add_done_callback(on_result)
def get_user_handler(user_id):
def on_user(user):
def on_orders(orders):
def on_cache(cache):
print(user, orders, cache)
query_cache(user['id'], on_cache)
query_orders(user['id'], on_orders)
query_user(user_id, on_user)
loop = asyncio.get_event_loop()
loop.run_until_complete(get_user_handler(1))
三层回调嵌套,可读性和可维护性都较差。
更关键的问题是错误传播。回调函数中抛出的异常不会自动向上传播,必须在每一层手动处理。实际项目中,这种模式容易导致异常被吞掉,问题难以定位。
node.js 早期同样采用回调模型,也因为同样的原因被广泛批评。
yield from 过渡方案
Python 3.4 引入了 @asyncio.coroutine 装饰器,配合 yield from 语法,可以将回调嵌套展平:
import asyncio
@asyncio.coroutine
def get_user(user_id):
conn = yield from async_mysql.connect(host='localhost', db='test')
cursor = yield from conn.cursor()
result = yield from cursor.execute(
'SELECT * FROM users WHERE id = %s',
(user_id,)
)
yield from conn.close()
return result
@asyncio.coroutine
def handler(user_id):
user = yield from get_user(user_id)
orders = yield from get_orders(user['id'])
cache = yield from get_cache(user['id'])
return user, orders, cache
loop = asyncio.get_event_loop()
result = loop.run_until_complete(handler(1))
比回调好读,但 yield from 的语义是「从生成器取值」,而这里实际做的是「让出控制权」。两个语义混在一起,理解成本较高。
而且 @asyncio.coroutine 是运行时检查,如果忘记加装饰器,或者函数里漏了 yield from,错误要到运行时才能发现。
这个方案是过渡性的,语法最终被 async/await 取代。
async/await
Python 3.5 引入 async def 和 await 语法,异步代码终于有了专用关键字:
import asyncio
import aiohttp
async def get_user(user_id):
async with aiohttp.ClientSession() as session:
async with session.get(
f'http://localhost:5000/user/{user_id}'
) as resp:
return await resp.json()
async def get_orders(user_id):
async with aiohttp.ClientSession() as session:
async with session.get(
f'http://localhost:5000/orders/{user_id}'
) as resp:
return await resp.json()
async def handler(user_id):
user, orders = await asyncio.gather(
get_user(user_id),
get_orders(user_id)
)
return user, orders
result = asyncio.run(handler(1))
async def 明确声明这是一个协程,await 明确表达「等待这个异步操作完成」。语义清晰,静态分析工具也能正确识别。
asyncio.gather 可以并发执行多个协程,而不是串行等待。
await 的底层机制
await 的本质是让出事件循环的控制权。
Python 有 GIL(全局解释器锁),同一时刻只有一个线程在执行 Python 字节码。但 await 会把当前协程挂起,事件循环把控制权交给其他就绪的协程。等 I/O 操作完成(通过底层回调通知),事件循环再把原协程唤醒,从 await 处继续执行。
事件循环的工作机制:

Event Loop 维护一个就绪队列。协程 await 时,被移出当前执行队列;I/O 完成后,通过回调重新加入队列。
所以异步是并发,不是并行。单线程内,多个协程交替执行,但在任意时刻只有一个协程在运行。
再压测一次异步版本:
$ ab -n 1000 -c 100 http://localhost:8000/user/1
Requests per second: 1247.83 [#/sec] (mean)
Time per request: 80.141 [ms] (mean)
1247 QPS,比同步版 42 QPS 高约 30 倍。提升来自于 I/O 等待时不占用线程,单线程可以处理大量并发连接。
几个需要注意的问题
在 async 函数中调用同步 I/O 会阻塞事件循环
async def bad():
time.sleep(1) # 阻塞整个事件循环
await asyncio.sleep(1) # 正确:让出控制权
time.sleep() 是同步阻塞调用,会卡住整个事件循环,所有其他协程都无法执行。
忘记 await 不会报语法错误,但运行时行为错误
async def fetch():
return 'data'
async def wrong():
result = fetch() # 返回协程对象,不是结果
result = await fetch() # 正确
fetch() 返回的是一个 coroutine 对象,必须 await 才会执行并拿到返回值。漏写 await 不会报语法错误,但运行时行为完全错误。
同步库和异步库不能混用
requests 是同步 HTTP 库,aiohttp 是异步 HTTP 库。在 async def 函数里调用 requests.get(),会阻塞事件循环。
uvloop 可以提升事件循环性能
import asyncio
import uvloop
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
uvloop 是用 Cython 实现的事件循环,性能是纯 Python asyncio 事件循环的 2-4 倍。生产环境建议开启。