title: python 实现协程异步编程的几种方式
协程(coroutine) ,也叫微进程, 是一种用户态内的上下文切换技术, 其实就是让一个线程可以实现代码块的相互切换执行。实现异步的效果
Greenlet 早期的python 协程实现
Greenlet 介绍:
Greenlet 是一个独立的第三方库,它为 Python 提供了一种早期的、非常底层的协程实现。它源自 Stackless Python,一个对 CPython 的修改版,旨在支持大规模并发。Greenlet 将 Stackless 的核心思想打包成一个 C 扩展模块,让标准的 CPython 也能使用这种微线程(greenlet)的能力。
与操作系统的线程不同,greenlet 是在用户空间内实现的轻量级并发单元。它们的调度是协作式的(Cooperative),意味着一个 greenlet 必须显式地通过调用 switch() 方法来放弃 CPU 控制权,并将执行权交给另一个 greenlet。这种明确的切换方式虽然给了程序员极大的控制力,但也使得代码逻辑变得复杂。
例子:
from greenlet import greenlet
import src.configs.config
from loguru import logger
# 定义第一个协程函数 func1
def func1():
# 打印日志 "step 1"
logger.info("step 1")
# 切换到第二个协程 gl2 执行
gl2.switch()
# 从其他协程切换回来后,打印日志 "step 3"
logger.info("step 3")
# 再次切换到第二个协程 gl2 执行,以完成 func2 的剩余部分,
# 注意, 如果没有这行代码,func1 执行完成后不会自己返回func2, func2 中的 "step 4" 将不会被执行
gl2.switch()
# 定义第二个协程函数 func2
def func2():
# 打印日志 "step 2"
logger.info("step 2")
# 切换回第一个协程 gl1 执行
gl1.switch()
# 从其他协程切换回来后,打印日志 "step 4"
logger.info("step 4")
# 创建第一个协程对象 gl1,关联 func1 函数
gl1 = greenlet(func1)
# 创建第二个协程对象 gl2,关联 func2 函数
gl2 = greenlet(func2)
# 定义一个 pytest 测试函数
def test_greenlet():
# 启动第一个协程 gl1,开始执行
gl1.switch()
output
test/async/test_greenlet.py 2025-10-13 23:51:41.413 | INFO | async.test_greenlet:func1:6 - step 1
2025-10-13 23:51:41.413 | INFO | async.test_greenlet:func2:13 - step 2
2025-10-13 23:51:41.413 | INFO | async.test_greenlet:func1:8 - step 3
2025-10-13 23:51:41.413 | INFO | async.test_greenlet:func2:15 - step 4
分析:
上面的例子清晰地展示了 greenlet 的协作式调度机制:
- 初始化:
gl1 = greenlet(func1)和gl2 = greenlet(func2)创建了两个独立的协程对象,但此时它们内部的代码都还没有开始执行。 - 启动:
gl1.switch()启动gl1,程序进入func1,打印 “step 1”。 - 第一次切换:
func1内部调用gl2.switch(),这会导致func1的执行被挂起,程序流程切换到gl2,进入func2并打印 “step 2”。 - 第二次切换:
func2内部调用gl1.switch(),func2被挂起,程序流程回到func1被挂起的地方,继续执行并打印 “step 3”。 - 第三次切换:
func1再次调用gl2.switch()。这一步非常关键。如果没有这次切换,func1执行完毕后,整个程序就会结束,func2中剩下的代码将永远不会被执行。通过这次切换,程序流程回到了func2被挂起的地方,继续执行并打印 “step 4”。 - 结束:
func2执行完毕,所有协程任务完成,程序退出。
总结:
- 优点:
Greenlet的上下文切换非常快,因为它完全在用户态进行,不涉及内核态的切换。它为 Python 带来了真正的协程概念。 - 缺点:
- 手动调度: 必须手动管理
switch调用,代码逻辑容易变得混乱,尤其是在复杂的场景下,容易出现忘记切换或死循环等问题。 - I/O 阻塞:
Greenlet本身并不能解决 I/O 阻塞问题。如果一个greenlet中执行了阻塞的 I/O 操作(如网络请求),它会阻塞整个操作系统线程,从而使所有其他的greenlet都无法执行。为了解决这个问题,通常需要配合Gevent这样的库,它通过monkey-patching(猴子补丁)的方式将标准库中的阻塞 I/O 函数替换为非阻塞版本。
- 手动调度: 必须手动管理
Greenlet 是 Python 异步编程演进史上的重要一步,它为后来更高级、更易用的异步框架(如 asyncio)奠定了基础。
yield 关键字实现协程
yield 介绍:
在 async/await 语法出现之前,Python 开发者巧妙地利用了生成器(Generator)的特性来模拟协程。这一切的核心就是 yield 关键字。
最初,yield 主要用于创建简单的迭代器。一个包含 yield 的函数会自动成为一个生成器函数,调用它会返回一个生成器对象。每当执行到 yield 语句时,函数会暂停执行并“产出”一个值。当外界通过 next() 再次请求值时,函数会从上次暂停的地方继续执行。
PEP 342 对生成器进行了增强,引入了 .send() 方法。这使得外界不仅可以从生成器取值,还可以向生成器内部发送值。这一改变是革命性的,因为它让生成器拥有了双向通信的能力,使其能够作为一种简单的协程使用:yield 可以暂停执行等待外部输入(例如,I/O 操作的结果),而 .send() 则可以把结果送回,让它继续执行。
后来,PEP 380 引入了 yield from 语法,极大地简化了在一个生成器中调用另一个生成器的代码,它允许一个生成器将其部分操作委托给另一个生成器。这正是 async/await 中 await 关键字的前身,它为构建更复杂的协程逻辑(如任务链)提供了基础。
[info] 重要规则
yield from后面必须跟一个可迭代对象,在协程的上下文中,这通常是一个生成器 (Generator)、协程 (Coroutine) 或 Future/Task 对象。
例子:
from loguru import logger
def generator1():
# First, the generator yields the value 1.
yield 1
# Then, it delegates execution to generator2.
# 'yield from' iterates over generator2 and yields all its values (3, 4).
yield from generator2()
# After generator2 is exhausted, generator1 resumes and yields the value 2.
yield 2
def generator2():
# This generator first yields the value 3.
yield 3
# Then it yields the value 4.
yield 4
# will return to generator1 after this.
def test_yield():
# Create a generator instance from generator1.
gen = generator1()
# Collect all yielded values from the generator into a list.
# This will execute the generator to completion:
# 1. Get 1 from generator1.
# 2. 'yield from generator2' starts.
# 3. Get 3 from generator2.
# 4. Get 4 from generator2.
# 5. 'yield from' finishes.
# 6. Get 2 from generator1.
# So, `results` will be [1, 3, 4, 2].
results = list(gen)
# Log the generated results.
logger.info(f"Generator results: {results}")
# Assert that the results list matches the expected sequence.
assert results == [1, 3, 4, 2]
output:
test/async/test_yield.py 2025-10-14 00:09:10.972 | INFO | async.test_yield:test_yield:21 - Generator results: [1, 3, 4, 2]
分析:
这个例子展示了 yield 和 yield from 如何协同工作来控制执行流程:
- 创建生成器:
gen = generator1()创建了generator1的生成器实例,但此时函数内的代码并未执行。 - 执行与暂停:
list(gen)开始迭代生成器。- 程序进入
generator1,执行到yield 1。生成器产出值1并在此处暂停。 - 迭代继续,程序回到
generator1,执行到yield from generator2()。
- 程序进入
- 委托执行:
yield from将执行权委托给generator2。- 程序进入
generator2,执行到yield 3。generator2产出值3并暂停。这个值被直接传递给list()。 - 迭代继续,程序回到
generator2,执行到yield 4。generator2产出值4并暂停。 generator2执行完毕,yield from表达式结束。
- 程序进入
- 恢复执行: 控制权返回给
generator1。generator1从yield from之后的位置继续执行,遇到yield 2。它产出值2并暂停。
- 结束:
generator1执行完毕,迭代结束。最终list()收集到所有产出的值,得到[1, 3, 4, 2]。
总结:
- 优点:
- 原生支持: 作为 Python 的原生语法,不需要引入任何第三方库。
- 概念基础: 它为 Python 社区引入并普及了“暂停/恢复”的协程核心思想,是
asyncio的重要基石。
- 缺点:
- 语义模糊: 生成器和协程共用一套语法,容易引起混淆。代码的意图(是迭代还是异步)不够明确。
- 需要调度器: 和
Greenlet一样,yield只提供了暂停和恢复执行的能力,但它本身不能实现异步 I/O。要实现真正的非阻塞并发,仍然需要一个外部的事件循环(Event Loop)来调度这些生成器。当一个生成器yield等待 I/O 时,事件循环会接管控制权,去执行其他就绪的生成器,并在 I/O 完成后,通过.send()将结果送回,唤醒等待的生成器。早期的异步框架(如 Tornado)就是基于这种模式构建的。
yield 和 yield from 是从传统同步编程迈向现代异步编程的关键一步,它们在语法层面为 async/await 的诞生铺平了道路。
asyncio (python 3.4 开始支持)
asyncio 介绍:
asyncio 是在 Python 3.4 中被引入标准库的,它为 Python 带来了官方的、统一的异步编程框架。它的出现标志着 Python 异步编程从零散的、基于第三方库的实现(如 Tornado, Gevent)走向了标准化。
asyncio 的核心是一个事件循环(Event Loop),它负责调度和执行异步任务。开发者通过定义**协程(Coroutine)**来编写异步代码。在 Python 3.4 中,协程是基于生成器的,需要使用 @asyncio.coroutine 装饰器来标记,并使用 yield from 来等待其他协程或 Future 对象。
PEP 492 在 Python 3.5 中引入了原生的 async 和 await 关键字,这是一个巨大的进步。async def 用于定义一个原生协程,await 用于暂停协程的执行,等待一个可等待对象(Awaitable)完成。这套新语法不仅让异步代码的意图更加清晰,也将其与普通的生成器彻底区分开来。
asyncio 不仅仅是一个语法糖,它提供了一整套用于处理 I/O、网络、子进程等操作的非阻塞工具,是构建高性能、高并发应用的基石。
例子:
import src.configs.config
from loguru import logger
import asyncio
import pytest
@asyncio.coroutine
def func1():
logger.info("step 1") # Log step 1
yield from asyncio.sleep(2) # Asynchronously wait for 2 seconds
logger.info("step 2") # Log step 2
@asyncio.coroutine
def func2():
logger.info("step 3") # Log step 3
yield from asyncio.sleep(2) # Asynchronously wait for 2 seconds
logger.info("step 4") # Log step 4
@pytest.mark.asyncio
@asyncio.coroutine
def test_asyncio():
tasks = [ # Create a list of coroutine tasks
func1(),
func2(),
]
yield from asyncio.gather(*tasks) # Run the tasks concurrently
# To make the file runnable for verification
if __name__ == '__main__':
loop = asyncio.get_event_loop()
loop.run_until_complete(test_asyncio())
output
platform linux -- Python 3.7.17, pytest-6.2.5, py-1.11.0, pluggy-1.0.0
rootdir: /home/gateman/projects/github/py-api-svc, configfile: pytest.ini
plugins: anyio-3.5.0, asyncio-0.20.3
test/async/test_asyncio.py 2025-10-14 23:54:51.790 | INFO | async.test_asyncio:func1:9 - step 1
2025-10-14 23:54:51.791 | INFO | async.test_asyncio:func2:15 - step 3
2025-10-14 23:54:53.792 | INFO | async.test_asyncio:func1:11 - step 2
2025-10-14 23:54:53.793 | INFO | async.test_asyncio:func2:17 - step 4
注意这里我切换成3.7 版本的python sdk 执行
因为:
虽然 asyncio 在 Python 3.4 中就已存在,但早期的 API 尚不稳定且在后续版本中经历了多次迭代和改进。
async/await语法的成熟: Python 3.5 引入了async/await语法,但直到 Python 3.7,async和await才正式成为保留关键字。这标志着这套语法已经完全成熟并成为 Python 异步编程的未来方向。- API 的稳定与增强: 到了 Python 3.7,
asyncio的 API 变得更加稳定和易用。例如,asyncio.run()函数被引入,它极大地简化了启动和运行顶层异步程序的代码,自动处理事件循环的创建和关闭。 - 生态系统的发展: 围绕
async/await的生态系统(如pytest-asyncio、aiohttp等)在 Python 3.7 时代已经非常繁荣,为开发者提供了强大的支持。
示例代码中使用的 @asyncio.coroutine 和 yield from 是 Python 3.4 时代的旧语法,虽然在 3.7 中仍然可用(为了向后兼容),但已不推荐。现代的 asyncio 代码应优先使用 async/await。这个例子主要是为了展示 asyncio 最初的形态。
分析:
这个例子展示了 asyncio 如何通过事件循环并发地执行多个任务:
- 任务创建: 在
test_asyncio函数中,tasks = [func1(), func2()]创建了两个协程对象。此时,func1和func2内部的代码都还没有执行。 - 并发调度:
yield from asyncio.gather(*tasks)是核心。asyncio.gather将多个协程(或其他可等待对象)包装成一个任务,并提交给事件循环。事件循环会并发地运行这些任务。 - 执行与挂起:
- 事件循环首先运行
func1。func1打印 “step 1”,然后遇到yield from asyncio.sleep(2)。asyncio.sleep()是一个非阻塞的延时函数,它会返回一个协程。yield from会暂停func1的执行,并将控制权交还给事件循环,同时告诉事件循环在 2 秒后唤醒它。 - 由于
func1被挂起,事件循环并没有闲置,它会查找其他可运行的任务。此时func2已经就绪。 - 事件循环开始运行
func2。func2打印 “step 3”,同样也遇到了yield from asyncio.sleep(2),于是func2也被挂起,控制权再次回到事件循环。
- 事件循环首先运行
- 等待与唤醒: 现在,两个任务都处于挂起状态,等待
sleep完成。事件循环会监控这些等待事件。 - 恢复执行: 大约 2 秒后,
sleep操作完成。事件循环会按照完成的顺序(或实现定义的顺序)唤醒挂起的任务。func1被唤醒,从yield from之后继续执行,打印 “step 2”。func2被唤醒,继续执行并打印 “step 4”。
- 任务完成: 当
func1和func2都执行完毕后,asyncio.gather任务也随之完成。整个test_asyncio函数执行结束。
从输出结果可以看到,“step 1” 和 “step 3” 几乎是同时打印的,然后程序等待了 2 秒,再几乎同时打印 “step 2” 和 “step 4”。这清晰地证明了 asyncio 实现了任务的并发执行,而不是顺序执行。在等待 I/O(本例中为 asyncio.sleep)时,CPU 没有被阻塞,而是被用来执行其他任务。
为什么asyncio 的出现是一个里程碑
相比于 greenlet 和原生 yield,asyncio 的出现是革命性的,因为它提供了一个标准化、自带电池的异步编程框架。
- 自动化的事件循环:
asyncio的核心是事件循环,它取代了greenlet中繁琐的手动switch()调用。开发者不再需要关心协程之间如何切换,只需要告诉事件循环哪些任务需要运行。 - 明确的 I/O 等待: 当一个
asyncio协程遇到 I/O 操作时(例如,await asyncio.sleep(2)或await session.get(url)),它会自动挂起,并将控制权交还给事件循环。事件循环此时并不会阻塞,而是会立即去执行下一个已经就绪的协程。当原来的 I/O 操作完成后,事件循环会得到通知,并在合适的时机唤醒之前挂起的协程,让它从等待处继续执行。这种“遇到 I/O 就放手,让别人先干”的模式,是实现高并发的关键。
为了更清晰地展示 asyncio 带来的性能优势,我们来看一个更实际的例子:同时发起两个网络请求。
模拟IO行为
我们构建一个 FastAPI 应用,其中包含一个异步端点,它会模拟一个耗时 2 秒的 I/O 操作。
import src.configs.config
from loguru import logger
import time
import asyncio
from datetime import datetime
from fastapi import APIRouter, Request
router = APIRouter()
@router.get("/async/test/{item_id}")
async def async_test(item_id: int,request: Request):
time_start = time.time()
await asyncio.sleep(2)
time_end = time.time()
# 格式化时间,并截取前3位微秒以得到毫秒
start_str = datetime.fromtimestamp(time_start).strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]
end_str = datetime.fromtimestamp(time_end).strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]
infostr = f"item id {item_id} started at: {start_str}, ended at: {end_str}, time costed:{time_end - time_start:.2f} seconds"
logger.info(infostr)
return {"message": "This is an async test endpoint.", "info": infostr}
解释:
这是一个典型的异步 Web API 端点:
- 使用
async def定义,使其成为一个原生协程。 await asyncio.sleep(2)模拟了一个耗时 2 秒的非阻塞 I/O 操作(例如,查询数据库、调用外部服务等)。在await期间,服务器进程不会被卡住,它可以去处理其他并发的请求。- 该端点记录并返回操作的开始和结束时间,方便我们观察其执行过程。
测试case 注意这里对比了异步和同步执行的耗时差异
下面的测试代码分别使用同步和异步方式调用上述 API 两次,并记录总耗时。
import http.client
import json
import time
import asyncio
import aiohttp
import pytest
from loguru import logger
# --- Synchronous Functions (Original) ---
def call_test_api(item_id: int):
"""Synchronously calls the test API and returns the response.
Args:
item_id (int): The ID of the item to request.
Returns:
dict: The JSON response from the API as a dictionary.
"""
conn = http.client.HTTPConnection("jpgcp.shop")
conn.request("GET", f"/pyapi/async/test/{item_id}")
response = conn.getresponse()
data = response.read().decode()
conn.close()
logger.info(f"Sync API response for item_id {item_id}: {data}")
return json.loads(data)
def sync_func1():
"""Wrapper function to synchronously call the API for item_id 1."""
logger.info("sync step 1")
call_test_api(1)
logger.info("sync step 2")
def sync_func2():
"""Wrapper function to synchronously call the API for item_id 2."""
logger.info("sync step 3")
call_test_api(2)
logger.info("sync step 4")
def test_sync():
"""Runs and times the synchronous API calls sequentially."""
logger.info("--- Starting Synchronous Test ---")
time_start = time.time()
sync_func1()
sync_func2()
time_end = time.time()
logger.info(f"All sync tasks completed in {time_end - time_start:.2f} seconds")
logger.info("--- Synchronous Test Finished ---")
# --- Asynchronous Functions (Legacy asyncio Style) ---
@asyncio.coroutine
def call_test_api_async(session, item_id: int):
"""Asynchronously calls the test API using legacy asyncio style.
Args:
session (aiohttp.ClientSession): The aiohttp client session.
item_id (int): The ID of the item to request.
Returns:
dict or None: The JSON response from the API as a dictionary,
or None if an error occurred.
"""
url = f"http://jpgcp.shop/pyapi/async/test/{item_id}"
logger.info(f"Async call for item_id: {item_id}")
try:
response = yield from session.get(url)
data = yield from response.json()
logger.info(f"Async API response for item_id {item_id}: {data}")
response.close()
return data
except aiohttp.ClientError as e:
logger.error(f"Async API call for item_id {item_id} failed: {e}")
return None
@asyncio.coroutine
def async_func1(session):
"""Async wrapper to call the API for item_id 1.
Args:
session (aiohttp.ClientSession): The aiohttp client session.
Returns:
dict or None: The result from the API call.
"""
logger.info("async step 1")
# `yield from` 将控制权交给事件循环来执行异步任务,是现代 `await` 关键字的前身。
result = yield from call_test_api_async(session, 1)
logger.info("async step 2")
return result
@asyncio.coroutine
def async_func2(session):
"""Async wrapper to call the API for item_id 2.
Args:
session (aiohttp.ClientSession): The aiohttp client session.
Returns:
dict or None: The result from the API call.
"""
logger.info("async step 3")
# `yield from` 将控制权交给事件循环来执行异步任务,是现代 `await` 关键字的前身。
result = yield from call_test_api_async(session, 2)
logger.info("async step 4")
return result
@pytest.mark.asyncio
@asyncio.coroutine
def test_asyncio():
"""Runs and times the asynchronous API calls concurrently."""
logger.info("--- Starting Asynchronous Test (Legacy Style) ---")
time_start = time.time()
session = aiohttp.ClientSession()
tasks = [
async_func1(session),
async_func2(session),
]
results = yield from asyncio.gather(*tasks)
yield from session.close()
time_end = time.time()
logger.info(f"All async tasks completed in {time_end - time_start:.2f} seconds")
logger.info(f"Async API results: {results}")
logger.info("--- Asynchronous Test (Legacy Style) Finished ---")
# --- Main Execution ---
if __name__ == "__main__":
# Run synchronous test
test_sync()
print("\n" + "="*50 + "\n")
# Run asynchronous test using the legacy event loop
loop = asyncio.get_event_loop()
loop.run_until_complete(test_asyncio())
output
(.venv37) gateman@MoreFine-S500: py-api-svc$ source .venv37/bin/activate && PYTHONPATH=. python test/async/test_asyncio2.py
2025-10-15 00:20:01.437 | INFO | __main__:test_sync:42 - --- Starting Synchronous Test ---
2025-10-15 00:20:01.437 | INFO | __main__:sync_func1:30 - sync step 1
2025-10-15 00:20:04.567 | INFO | __main__:call_test_api:25 - Sync API response for item_id 1: {"message":"This is an async test endpoint.","info":"item id 1 started at: 2025-10-14 16:20:02.391, ended at: 2025-10-14 16:20:04.392, time costed:2.00 seconds"}
2025-10-15 00:20:04.568 | INFO | __main__:sync_func1:32 - sync step 2
2025-10-15 00:20:04.568 | INFO | __main__:sync_func2:36 - sync step 3
2025-10-15 00:20:07.323 | INFO | __main__:call_test_api:25 - Sync API response for item_id 2: {"message":"This is an async test endpoint.","info":"item id 2 started at: 2025-10-14 16:20:05.155, ended at: 2025-10-14 16:20:07.156, time costed:2.00 seconds"}
2025-10-15 00:20:07.323 | INFO | __main__:sync_func2:38 - sync step 4
2025-10-15 00:20:07.323 | INFO | __main__:test_sync:47 - All sync tasks completed in 5.89 seconds
2025-10-15 00:20:07.324 | INFO | __main__:test_sync:48 - --- Synchronous Test Finished ---
2025-10-15 00:20:07.324 | INFO | __main__:test_asyncio:112 - --- Starting Asynchronous Test (Legacy Style) ---
2025-10-15 00:20:07.325 | INFO | __main__:async_func1:86 - async step 1
2025-10-15 00:20:07.325 | INFO | __main__:call_test_api_async:65 - Async call for item_id: 1
2025-10-15 00:20:07.325 | INFO | __main__:async_func2:102 - async step 3
2025-10-15 00:20:07.325 | INFO | __main__:call_test_api_async:65 - Async call for item_id: 2
2025-10-15 00:20:09.995 | INFO | __main__:call_test_api_async:69 - Async API response for item_id 2: {'message': 'This is an async test endpoint.', 'info': 'item id 2 started at: 2025-10-14 16:20:07.831, ended at: 2025-10-14 16:20:09.832, time costed:2.00 seconds'}
2025-10-15 00:20:09.995 | INFO | __main__:async_func2:105 - async step 4
2025-10-15 00:20:09.996 | INFO | __main__:call_test_api_async:69 - Async API response for item_id 1: {'message': 'This is an async test endpoint.', 'info': 'item id 1 started at: 2025-10-14 16:20:07.816, ended at: 2025-10-14 16:20:09.818, time costed:2.00 seconds'}
2025-10-15 00:20:09.996 | INFO | __main__:async_func1:89 - async step 2
2025-10-15 00:20:09.996 | INFO | __main__:test_asyncio:124 - All async tasks completed in 2.67 seconds
2025-10-15 00:20:09.996 | INFO | __main__:test_asyncio:125 - Async API results: [{'message': 'This is an async test endpoint.', 'info': 'item id 1 started at: 2025-10-14 16:20:07.816, ended at: 2025-10-14 16:20:09.818, time costed:2.00 seconds'}, {'message': 'This is an async test endpoint.', 'info': 'item id 2 started at: 2025-10-14 16:20:07.831, ended at: 2025-10-14 16:20:09.832, time costed:2.00 seconds'}]
2025-10-15 00:20:09.997 | INFO | __main__:test_asyncio:126 - --- Asynchronous Test (Legacy Style) Finished ---
分析:
日志输出清晰地展示了同步阻塞与异步非阻塞的巨大差异:
-
同步测试 (Synchronous Test):
- 总耗时: 5.89 秒。这约等于两次 API 调用(每次 2 秒)的耗时总和,外加网络和其他开销。
- 执行流程: 日志显示,程序严格地按顺序执行。
sync_func1开始,发起第一个 API 调用,然后程序被阻塞,死等 2 秒多直到收到响应。在sync_func1完全结束后,sync_func2才开始,然后再次阻塞等待 2 秒多。CPU 在大部分时间里都在空闲等待 I/O,效率极低。
-
异步测试 (Asynchronous Test):
- 总耗时: 2.67 秒。这个时间仅比单次 API 调用的耗时(2 秒)略长一点。
- 执行流程: 日志显示,
async step 1和async step 3几乎在同一时刻 (00:20:07.325) 被打印。程序发起了第一个 API 调用 (item_id: 1),但并没有等待,而是立刻返回事件循环,事件循环马上又发起了第二个 API 调用 (item_id: 2)。 - 两个 API 请求在网络中是并行的。程序在这 2 秒的等待时间里,CPU 并没有被阻塞,而是可以去做其他事情(虽然本例中没有其他事可做)。
- 大约 2 秒后,两个 API 的响应几乎同时到达 (
00:20:09.995和00:20:09.996),程序被唤醒并处理结果。
这个对比有力地证明了 asyncio 的核心价值:通过并发执行 I/O 密集型任务,将原本需要串行等待的时间大幅重叠,从而极大地提升了程序的总吞吐量和效率。
4. async/await 关键字
asyncio 框架虽然强大,但早期基于生成器的 @asyncio.coroutine 和 yield from 语法有两个主要问题:
- 可读性差:它与普通生成器的语法完全一样,使得代码的意图(是用于迭代还是用于异步)变得模糊不清。
- 写法繁琐:
yield from相比于单个关键字来说,还是显得有些冗长。
为了解决这些问题,PEP 492 在 Python 3.5 中引入了全新的、专用于异步编程的 async 和 await 关键字。这标志着异步编程在 Python 中拥有了原生语法支持,成为了一等公民。
async def: 用于定义一个原生协程 (native coroutine)。用它定义的函数,其返回就是一个协程对象。await: 只能在async def函数内部使用,用于暂停当前协程的执行,等待一个可等待对象 (Awaitable)(如另一个协程、Future 或 Task)的完成。await完美地替代了yield from的功能,但语法更简洁,意图也更明确。
这套新语法在功能上与 yield from 等价,底层的事件循环机制也完全相同,但它在代码的可读性和易用性上带来了质的飞跃。
例子:用 async/await 重构
下面我们将前一个例子中的异步部分用现代的 async/await 语法进行重构,可以清晰地看到代码的变化。
# --- Asynchronous Functions (Modern async/await Style) ---
import aiohttp
import asyncio
from loguru import logger
import time
import pytest
async def call_test_api_modern_async(session, item_id: int):
"""Asynchronously calls the test API using modern async/await syntax."""
url = f"http://jpgcp.shop/pyapi/async/test/{item_id}"
logger.info(f"Modern async call for item_id: {item_id}")
try:
# 使用 async with 确保 session 和 response 被正确管理
async with session.get(url) as response:
# await 替代了 yield from
data = await response.json()
logger.info(f"Modern async API response for item_id {item_id}: {data}")
return data
except aiohttp.ClientError as e:
logger.error(f"Modern async API call for item_id {item_id} failed: {e}")
return None
async def async_func1_modern(session):
"""Modern async wrapper to call the API for item_id 1."""
logger.info("modern async step 1")
# await 替代了 yield from
result = await call_test_api_modern_async(session, 1)
logger.info("modern async step 2")
return result
async def async_func2_modern(session):
"""Modern async wrapper to call the API for item_id 2."""
logger.info("modern async step 3")
# await 替代了 yield from
result = await call_test_api_modern_async(session, 2)
logger.info("modern async step 4")
return result
@pytest.mark.asyncio
async def test_modern_asyncio():
"""Runs and times the modern asynchronous API calls concurrently."""
logger.info("--- Starting Asynchronous Test (Modern Style) ---")
time_start = time.time()
# 使用 async with 自动管理 session 的生命周期
async with aiohttp.ClientSession() as session:
tasks = [
async_func1_modern(session),
async_func2_modern(session),
]
# await 替代了 yield from
results = await asyncio.gather(*tasks)
time_end = time.time()
logger.info(f"All modern async tasks completed in {time_end - time_start:.2f} seconds")
logger.info(f"Modern async API results: {results}")
logger.info("--- Asynchronous Test (Modern Style) Finished ---")
# To make the file runnable for verification
if __name__ == '__main__':
# 在 Python 3.7+ 中,可以直接使用 asyncio.run() 来运行顶层异步函数
asyncio.run(test_modern_asyncio())
分析
对比 yield from 的旧风格,async/await 的新风格带来了显而易见的改进:
-
定义协程:
- 旧:
@asyncio.coroutine装饰器 +def - 新:
async def
async def的意图非常明确:这不再是一个普通的函数或生成器,而是一个原生协程。
- 旧:
-
等待与暂停:
- 旧:
yield from <协程> - 新:
await <协程>
await关键字更短、更清晰,并且只能在async def函数中使用,避免了在错误上下文(如普通函数)中误用yield from的可能。
- 旧:
-
上下文管理:
- 新风格引入了
async with和async for,使得异步环境下的资源管理(如aiohttp.ClientSession)和迭代变得像同步代码一样直观和安全。
- 新风格引入了
总而言之,async/await 并没有改变 asyncio 的核心工作原理(事件循环和协程调度),但它提供了一层优雅的语法糖,极大地改善了开发者的编程体验,降低了异步编程的心智负担,是 Python 异步发展史上最重要的一步。
考虑到 async/await 是当今最流行和最被推荐的异步框架,其更多高级用法(如任务控制、同步原语等)和生态细节我会另起一文详细介绍。
本文总结
本文档回顾了 Python 异步编程从早期探索到现代实践的完整演进历程,主要涵盖了四个关键阶段:
-
Greenlet: 作为早期探索者,Greenlet引入了用户态协程的概念,但需要开发者进行复杂且易错的手动调度。 -
yield/yield from: 随后,Python 利用自身的生成器特性来模拟协程。这虽然是原生实现,但语法上存在歧义,且同样需要一个外部的事件循环来驱动。 -
asyncio(旧式): Python 3.4 带来的asyncio框架是第一个官方标准,它提供了内置的事件循环,实现了自动化调度。然而,它依然使用基于生成器的@asyncio.coroutine和yield from语法,可读性有待提高。 -
async/await(现代): Python 3.5 引入的async和await关键字是决定性的里程碑。它们提供了原生、清晰的异步语法,将异步代码与普通生成器彻底区分开,极大地提升了代码的可读性和开发体验,并催生了繁荣的现代异步生态。
总而言之,Python 的异步编程从一个需要高深技巧的底层操作,逐步演化为一门优雅、强大且易于上手的语言特性。其核心思想——通过协作式多任务处理 I/O 密集型负载——始终未变,但实现方式的不断优化最终使其成为 Python 在高并发领域的核心竞争力。
1403

被折叠的 条评论
为什么被折叠?



