python异步编程-- 实现协程异步编程的几种方式

部署运行你感兴趣的模型镜像

title: python 实现协程异步编程的几种方式

协程(coroutine) ,也叫微进程, 是一种用户态内的上下文切换技术, 其实就是让一个线程可以实现代码块的相互切换执行。实现异步的效果

Greenlet 早期的python 协程实现

Greenlet 介绍:
Greenlet 是一个独立的第三方库,它为 Python 提供了一种早期的、非常底层的协程实现。它源自 Stackless Python,一个对 CPython 的修改版,旨在支持大规模并发。GreenletStackless 的核心思想打包成一个 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 的协作式调度机制:

  1. 初始化: gl1 = greenlet(func1)gl2 = greenlet(func2) 创建了两个独立的协程对象,但此时它们内部的代码都还没有开始执行。
  2. 启动: gl1.switch() 启动 gl1,程序进入 func1,打印 “step 1”。
  3. 第一次切换: func1 内部调用 gl2.switch(),这会导致 func1 的执行被挂起,程序流程切换到 gl2,进入 func2 并打印 “step 2”。
  4. 第二次切换: func2 内部调用 gl1.switch()func2 被挂起,程序流程回到 func1 被挂起的地方,继续执行并打印 “step 3”。
  5. 第三次切换: func1 再次调用 gl2.switch()这一步非常关键。如果没有这次切换,func1 执行完毕后,整个程序就会结束,func2 中剩下的代码将永远不会被执行。通过这次切换,程序流程回到了 func2 被挂起的地方,继续执行并打印 “step 4”。
  6. 结束: 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/awaitawait 关键字的前身,它为构建更复杂的协程逻辑(如任务链)提供了基础。

[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]

分析:
这个例子展示了 yieldyield from 如何协同工作来控制执行流程:

  1. 创建生成器: gen = generator1() 创建了 generator1 的生成器实例,但此时函数内的代码并未执行。
  2. 执行与暂停: list(gen) 开始迭代生成器。
    • 程序进入 generator1,执行到 yield 1。生成器产出值 1 并在此处暂停。
    • 迭代继续,程序回到 generator1,执行到 yield from generator2()
  3. 委托执行: yield from 将执行权委托给 generator2
    • 程序进入 generator2,执行到 yield 3generator2 产出值 3 并暂停。这个值被直接传递给 list()
    • 迭代继续,程序回到 generator2,执行到 yield 4generator2 产出值 4 并暂停。
    • generator2 执行完毕,yield from 表达式结束。
  4. 恢复执行: 控制权返回给 generator1
    • generator1yield from 之后的位置继续执行,遇到 yield 2。它产出值 2 并暂停。
  5. 结束: generator1 执行完毕,迭代结束。最终 list() 收集到所有产出的值,得到 [1, 3, 4, 2]

总结:

  • 优点:
    • 原生支持: 作为 Python 的原生语法,不需要引入任何第三方库。
    • 概念基础: 它为 Python 社区引入并普及了“暂停/恢复”的协程核心思想,是 asyncio 的重要基石。
  • 缺点:
    • 语义模糊: 生成器和协程共用一套语法,容易引起混淆。代码的意图(是迭代还是异步)不够明确。
    • 需要调度器: 和 Greenlet 一样,yield 只提供了暂停和恢复执行的能力,但它本身不能实现异步 I/O。要实现真正的非阻塞并发,仍然需要一个外部的事件循环(Event Loop)来调度这些生成器。当一个生成器 yield 等待 I/O 时,事件循环会接管控制权,去执行其他就绪的生成器,并在 I/O 完成后,通过 .send() 将结果送回,唤醒等待的生成器。早期的异步框架(如 Tornado)就是基于这种模式构建的。

yieldyield 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 中引入了原生的 asyncawait 关键字,这是一个巨大的进步。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 尚不稳定且在后续版本中经历了多次迭代和改进。

  1. async/await 语法的成熟: Python 3.5 引入了 async/await 语法,但直到 Python 3.7,asyncawait 才正式成为保留关键字。这标志着这套语法已经完全成熟并成为 Python 异步编程的未来方向。
  2. API 的稳定与增强: 到了 Python 3.7,asyncio 的 API 变得更加稳定和易用。例如,asyncio.run() 函数被引入,它极大地简化了启动和运行顶层异步程序的代码,自动处理事件循环的创建和关闭。
  3. 生态系统的发展: 围绕 async/await 的生态系统(如 pytest-asyncioaiohttp 等)在 Python 3.7 时代已经非常繁荣,为开发者提供了强大的支持。

示例代码中使用的 @asyncio.coroutineyield from 是 Python 3.4 时代的旧语法,虽然在 3.7 中仍然可用(为了向后兼容),但已不推荐。现代的 asyncio 代码应优先使用 async/await。这个例子主要是为了展示 asyncio 最初的形态。

分析:
这个例子展示了 asyncio 如何通过事件循环并发地执行多个任务:

  1. 任务创建: 在 test_asyncio 函数中,tasks = [func1(), func2()] 创建了两个协程对象。此时,func1func2 内部的代码都还没有执行。
  2. 并发调度: yield from asyncio.gather(*tasks) 是核心。asyncio.gather 将多个协程(或其他可等待对象)包装成一个任务,并提交给事件循环。事件循环会并发地运行这些任务。
  3. 执行与挂起:
    • 事件循环首先运行 func1func1 打印 “step 1”,然后遇到 yield from asyncio.sleep(2)asyncio.sleep() 是一个非阻塞的延时函数,它会返回一个协程。yield from 会暂停 func1 的执行,并将控制权交还给事件循环,同时告诉事件循环在 2 秒后唤醒它。
    • 由于 func1 被挂起,事件循环并没有闲置,它会查找其他可运行的任务。此时 func2 已经就绪。
    • 事件循环开始运行 func2func2 打印 “step 3”,同样也遇到了 yield from asyncio.sleep(2),于是 func2 也被挂起,控制权再次回到事件循环。
  4. 等待与唤醒: 现在,两个任务都处于挂起状态,等待 sleep 完成。事件循环会监控这些等待事件。
  5. 恢复执行: 大约 2 秒后,sleep 操作完成。事件循环会按照完成的顺序(或实现定义的顺序)唤醒挂起的任务。
    • func1 被唤醒,从 yield from 之后继续执行,打印 “step 2”。
    • func2 被唤醒,继续执行并打印 “step 4”。
  6. 任务完成: 当 func1func2 都执行完毕后,asyncio.gather 任务也随之完成。整个 test_asyncio 函数执行结束。

从输出结果可以看到,“step 1” 和 “step 3” 几乎是同时打印的,然后程序等待了 2 秒,再几乎同时打印 “step 2” 和 “step 4”。这清晰地证明了 asyncio 实现了任务的并发执行,而不是顺序执行。在等待 I/O(本例中为 asyncio.sleep)时,CPU 没有被阻塞,而是被用来执行其他任务。





为什么asyncio 的出现是一个里程碑

相比于 greenlet 和原生 yieldasyncio 的出现是革命性的,因为它提供了一个标准化、自带电池的异步编程框架。

  • 自动化的事件循环: 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 ---

分析:
日志输出清晰地展示了同步阻塞与异步非阻塞的巨大差异:

  1. 同步测试 (Synchronous Test):

    • 总耗时: 5.89 秒。这约等于两次 API 调用(每次 2 秒)的耗时总和,外加网络和其他开销。
    • 执行流程: 日志显示,程序严格地按顺序执行。sync_func1 开始,发起第一个 API 调用,然后程序被阻塞,死等 2 秒多直到收到响应。在 sync_func1 完全结束后,sync_func2 才开始,然后再次阻塞等待 2 秒多。CPU 在大部分时间里都在空闲等待 I/O,效率极低。
  2. 异步测试 (Asynchronous Test):

    • 总耗时: 2.67 秒。这个时间仅比单次 API 调用的耗时(2 秒)略长一点。
    • 执行流程: 日志显示,async step 1async step 3 几乎在同一时刻 (00:20:07.325) 被打印。程序发起了第一个 API 调用 (item_id: 1),但并没有等待,而是立刻返回事件循环,事件循环马上又发起了第二个 API 调用 (item_id: 2)。
    • 两个 API 请求在网络中是并行的。程序在这 2 秒的等待时间里,CPU 并没有被阻塞,而是可以去做其他事情(虽然本例中没有其他事可做)。
    • 大约 2 秒后,两个 API 的响应几乎同时到达 (00:20:09.99500:20:09.996),程序被唤醒并处理结果。

这个对比有力地证明了 asyncio 的核心价值:通过并发执行 I/O 密集型任务,将原本需要串行等待的时间大幅重叠,从而极大地提升了程序的总吞吐量和效率。





4. async/await 关键字

asyncio 框架虽然强大,但早期基于生成器的 @asyncio.coroutineyield from 语法有两个主要问题:

  1. 可读性差:它与普通生成器的语法完全一样,使得代码的意图(是用于迭代还是用于异步)变得模糊不清。
  2. 写法繁琐yield from 相比于单个关键字来说,还是显得有些冗长。

为了解决这些问题,PEP 492Python 3.5 中引入了全新的、专用于异步编程的 asyncawait 关键字。这标志着异步编程在 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 的新风格带来了显而易见的改进:

  1. 定义协程:

    • 旧: @asyncio.coroutine 装饰器 + def
    • 新: async def
      async def 的意图非常明确:这不再是一个普通的函数或生成器,而是一个原生协程
  2. 等待与暂停:

    • 旧: yield from <协程>
    • 新: await <协程>
      await 关键字更短、更清晰,并且只能在 async def 函数中使用,避免了在错误上下文(如普通函数)中误用 yield from 的可能。
  3. 上下文管理:

    • 新风格引入了 async withasync for,使得异步环境下的资源管理(如 aiohttp.ClientSession)和迭代变得像同步代码一样直观和安全。

总而言之,async/await 并没有改变 asyncio 的核心工作原理(事件循环和协程调度),但它提供了一层优雅的语法糖,极大地改善了开发者的编程体验,降低了异步编程的心智负担,是 Python 异步发展史上最重要的一步。

考虑到 async/await 是当今最流行和最被推荐的异步框架,其更多高级用法(如任务控制、同步原语等)和生态细节我会另起一文详细介绍。





本文总结

本文档回顾了 Python 异步编程从早期探索到现代实践的完整演进历程,主要涵盖了四个关键阶段:

  1. Greenlet: 作为早期探索者,Greenlet 引入了用户态协程的概念,但需要开发者进行复杂且易错的手动调度

  2. yield/yield from: 随后,Python 利用自身的生成器特性来模拟协程。这虽然是原生实现,但语法上存在歧义,且同样需要一个外部的事件循环来驱动。

  3. asyncio (旧式): Python 3.4 带来的 asyncio 框架是第一个官方标准,它提供了内置的事件循环,实现了自动化调度。然而,它依然使用基于生成器的 @asyncio.coroutineyield from 语法,可读性有待提高。

  4. async/await (现代): Python 3.5 引入的 asyncawait 关键字是决定性的里程碑。它们提供了原生、清晰的异步语法,将异步代码与普通生成器彻底区分开,极大地提升了代码的可读性和开发体验,并催生了繁荣的现代异步生态。

总而言之,Python 的异步编程从一个需要高深技巧的底层操作,逐步演化为一门优雅、强大且易于上手的语言特性。其核心思想——通过协作式多任务处理 I/O 密集型负载——始终未变,但实现方式的不断优化最终使其成为 Python 在高并发领域的核心竞争力。

您可能感兴趣的与本文相关的镜像

Python3.9

Python3.9

Conda
Python

Python 是一种高级、解释型、通用的编程语言,以其简洁易读的语法而闻名,适用于广泛的应用,包括Web开发、数据分析、人工智能和自动化脚本

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

nvd11

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值