Python 协程调度:从 yield 到 async/await,理解协程的运行时机制

Python 协程调度:从 yield 到 async/await,理解协程的运行时机制

cover

一、协程的困惑:为什么 async def 不是多线程

Python 的协程(coroutine)是最容易被误解的并发机制。很多人以为 async def 就是"自动多线程",但实际上协程是单线程的——同一时刻只有一个协程在执行,协程之间的切换是协作式的(在 await 点主动让出),而非抢占式的(操作系统强制切换)。

协程的核心价值不是并行计算,而是高效等待:当一个协程在等待 IO(网络请求、数据库查询)时,它可以主动让出执行权,让事件循环调度其他协程运行。等待结束后,事件循环再恢复该协程。这样,单线程就能处理数千个并发连接,而每个线程的内存开销只有 2KB(vs 线程的 8MB)。

二、Python 协程的演进:从生成器到原生协程

flowchart TB
    A[Python 协程演进] --> B[生成器 yield]
    B --> C[yield from 委托]
    C --> D[async/await 原生协程]

    B --> B1[Python 2.5: yield 暂停函数执行]
    C --> C1[Python 3.3: yield from 委托子生成器]
    D --> D1[Python 3.5: async def + await]

    D --> E[事件循环 asyncio]
    E --> E1[注册协程到循环]
    E --> E2[调度: await 时切换]
    E --> E3[IO 就绪后恢复]

    style D fill:#ff6b6b,color:#fff
    style E fill:#4d96ff,color:#fff

三代协程的对比:

  • yield 生成器:函数中使用 yield 暂停执行,调用方通过 send() 恢复。本质是迭代器,但可以用来实现简单的协程。缺点是语法不直观,无法区分"生成数据"和"暂停执行"。
  • yield from 委托:允许生成器将部分操作委托给子生成器,简化了协程的组合。但仍基于生成器,语义不够清晰。
  • async/await 原生协程:Python 3.5 引入,async def 定义协程函数,await 等待可等待对象。语法清晰、语义明确,是现代 Python 异步编程的标准。

三、协程调度机制实现

# 从零实现一个简化的事件循环,理解协程调度原理
import time
from collections import deque
from typing import Coroutine, Any


class SimpleEventLoop:
    """简化的事件循环:理解协程调度的核心机制"""

    def __init__(self):
        self.ready_queue: deque[Coroutine] = deque()
        self.running = False

    def create_task(self, coro: Coroutine):
        """将协程加入就绪队列"""
        self.ready_queue.append(coro)

    def run(self):
        """运行事件循环,直到所有协程完成"""
        self.running = True

        while self.ready_queue:
            # 取出就绪队列中的下一个协程
            coro = self.ready_queue.popleft()

            try:
                # 推进协程执行,直到下一个 await 点
                # send(None) 等同于 next(coro)
                result = coro.send(None)

                # result 是 await 表达式返回的值
                # 在真实事件循环中,这里会处理 Future/Task
                # 简化实现:直接将协程重新加入队列
                self.ready_queue.append(coro)

            except StopIteration as e:
                # 协程执行完毕,返回值在 e.value 中
                pass
            except Exception as e:
                # 协程抛出异常
                print(f"协程异常: {e}")


# 简化的 awaitable 对象
class WaitForSeconds:
    """模拟 asyncio.sleep 的简化实现"""

    def __init__(self, seconds: float):
        self.seconds = seconds
        self.start_time = None

    def __await__(self):
        # __await__ 返回一个迭代器,事件循环通过 send() 驱动
        self.start_time = time.time()
        while time.time() - self.start_time < self.seconds:
            # 还没到时间,让出执行权
            yield
        # 时间到了,返回 None
        return None


# 使用简化事件循环
async def task(name: str, count: int):
    for i in range(count):
        print(f"[{name}] 步骤 {i+1}/{count}")
        await WaitForSeconds(0.1)  # 模拟异步等待

    print(f"[{name}] 完成!")
    return f"{name} done"


loop = SimpleEventLoop()
loop.create_task(task("A", 3))
loop.create_task(task("B", 2))
loop.run()
# 输出:A 和 B 交替执行,而非顺序执行
# 真实事件循环的关键机制 — asyncio 源码级理解
import asyncio
import time


# 机制一:Future — 异步结果的容器
async def future_example():
    """Future 是 asyncio 的底层原语,代表一个未来的结果"""
    loop = asyncio.get_event_loop()

    # 创建 Future
    future = loop.create_future()

    # 模拟异步操作完成后设置结果
    loop.call_later(1.0, future.set_result, "操作完成")

    # await 等待 Future 完成
    result = await future
    print(f"Future 结果: {result}")


# 机制二:Task — 协程的调度包装
async def task_example():
    """Task 是对协程的包装,由事件循环调度执行"""
    # create_task 将协程包装为 Task 并立即开始调度
    task1 = asyncio.create_task(fetch_data("API-1"))
    task2 = asyncio.create_task(fetch_data("API-2"))

    # 并发执行两个任务
    result1, result2 = await asyncio.gather(task1, task2)
    print(f"结果: {result1}, {result2}")


async def fetch_data(name: str) -> str:
    await asyncio.sleep(0.5)
    return f"{name} 数据"


# 机制三:gather vs wait — 批量调度的两种模式
async def gather_vs_wait():
    """gather 和 wait 的区别"""

    tasks = [fetch_data(f"API-{i}") for i in range(5)]

    # gather:等待全部完成,保持顺序
    results = await asyncio.gather(*tasks)
    print(f"gather 结果顺序: {results}")

    # wait:更灵活,可指定返回条件
    done, pending = await asyncio.wait(
        tasks,
        timeout=2.0,               # 超时控制
        return_when=asyncio.FIRST_COMPLETED  # 最早完成时返回
    )
    print(f"wait 最早完成: {len(done)} 个")


# 机制四:协程取消
async def cancellation_example():
    """协程取消与清理"""
    task = asyncio.create_task(long_running_operation())

    # 1 秒后取消
    await asyncio.sleep(1.0)
    task.cancel()

    try:
        await task
    except asyncio.CancelledError:
        print("任务已取消")


async def long_running_operation():
    try:
        for i in range(100):
            await asyncio.sleep(0.1)
            print(f"处理中... {i}")
    except asyncio.CancelledError:
        # 清理资源
        print("收到取消信号,清理资源...")
        raise  # 重新抛出,让调用方知道任务已取消
# 协程调度性能对比 — 协程 vs 线程 vs 进程
import asyncio
import time
import threading
import multiprocessing


async def async_io_task():
    """协程 IO 任务"""
    await asyncio.sleep(0.01)
    return 1


def thread_io_task():
    """线程 IO 任务"""
    time.sleep(0.01)
    return 1


def benchmark():
    """对比三种并发模型的吞吐"""
    task_count = 1000

    # 协程
    start = time.time()
    async def run_async():
        tasks = [async_io_task() for _ in range(task_count)]
        await asyncio.gather(*tasks)
    asyncio.run(run_async())
    async_time = time.time() - start

    # 线程
    start = time.time()
    threads = [threading.Thread(target=thread_io_task) for _ in range(task_count)]
    for t in threads:
        t.start()
    for t in threads:
        t.join()
    thread_time = time.time() - start

    print(f"协程: {async_time:.3f}s, 线程: {thread_time:.3f}s")
    # 典型输出:协程 0.02s, 线程 1.5s
    # 协程快 70 倍,因为线程的创建和切换开销远大于协程

四、协程调度的工程陷阱

阻塞调用杀死事件循环:在 async 函数中调用 time.sleep()requests.get()subprocess.run() 等同步阻塞函数,会阻塞整个事件循环,所有协程都无法推进。必须用 asyncio.sleep()aiohttpasyncio.create_subprocess_exec() 替代,或用 asyncio.to_thread() 将阻塞调用移到线程池。

协程泄漏asyncio.create_task() 创建的 Task 如果没有被 await,其异常会被静默吞掉。解决方案是在 Python 3.11+ 中使用 TaskGroup,它会自动等待所有子任务完成并传播异常。

GIL 下的 CPU 密集型:协程无法利用多核——Python 的 GIL 限制了同一时刻只有一个线程执行 Python 代码。CPU 密集型任务应使用 ProcessPoolExecutor + asyncio.get_event_loop().run_in_executor(),将计算移到独立进程中。

事件循环的线程安全asyncio 的事件循环不是线程安全的。从其他线程向事件循环提交任务,必须使用 asyncio.run_coroutine_threadsafe(),而非直接 create_task()。跨线程操作事件循环是 Bug 的高发区。

五、总结

Python 协程调度的核心原则:单线程协作式调度、await 是让出点、阻塞操作必须隔离。落地建议:

  1. 理解机制:协程是单线程的,await 是主动让出,事件循环负责调度。这不是魔法,是协作式并发。
  2. 消灭阻塞:async 函数中禁止同步 IO,用 asyncio.to_thread() 隔离无法避免的阻塞调用。
  3. 批量调度:用 gather 并发等待全部、wait 灵活控制返回条件、TaskGroup 自动管理生命周期。
  4. CPU 密集型:用 ProcessPoolExecutor 将计算移到进程池,协程只负责调度和 IO。

协程不是万能的并发方案,但在 IO 密集型场景中,它是 Python 最轻量、最高效的选择。理解调度机制,才能写出正确的异步代码。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值