首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Python 协程的底层原理

Python 协程的底层原理

作者头像
YaoQi
发布2025-11-13 17:46:03
发布2025-11-13 17:46:03
1230
举报

Python 协程,是可以在执行过程中暂停和恢复的特殊函数,让单个线程能够并发处理多个任务。

可以使用 async 和 await 关键字来定义协程函数:

代码语言:javascript
复制
import asyncio

async def task1():
    for i in range(3):
        await asyncio.sleep(1)
        print(f"任务1: 第{i + 1}步")

async def task2():
    for i in range(3):
        await asyncio.sleep(0.5)
        print(f"任务2: 第{i + 1}步")

async def main():
    # 同时运行多个协程
    await asyncio.gather(
        task1(),
        task2()
    )

asyncio.run(main())

协程运行步骤:

  1. 挂起当前协程
    • 运行到 await 语句,当前协程暂停执行
    • 控制权返回给事件循环
  2. 调度可等待对象
    • some_awaitable 被注册到事件循环中
    • 事件循环开始监视这个可等待对象的状态
  3. 事件循环继续运行
    • 事件循环可以执行其他就绪的协程
    • 同时等待 some_awaitable 完成
  4. 恢复执行
    • some_awaitable 完成时,事件循环重新激活挂起的协程
    • 协程从 await 语句后继续执行

可见,事件循环(Event loop)是协程调度的核心

事件循环包含一组待运行的作业。事件循环会从其待处理事项中取出一个作业并唤起它(或称“给予其控制权”),类似于调用一个函数,然后该作业就会运行。 一旦它暂停或完成,它会将控制权返回给事件循环。 然后事件循环会从作业池中选择另一个作业并唤起它。

此过程将无限地重复,事件循环也不停地循环下去。 如果没有待执行的作业,事件循环会足够智能地转入休息状态以避免浪费 CPU 周期,并在有更多工作需完成时恢复运行。

除了官方asyncio库,还有第三方协程库比如trio。

关于事件循环,这里有一个概念关系图,它们都是基于操作系统底层的事件通知机制:

关键字的说明

async def 定义的函数是协程函数,任何函数都可以用 async def 定义,但如果其内部及其调用的函数,没有交出控制权的 await ,一旦其开始运行,直到结束才会交出控制权(默认的return语句交出的)。

调用协程函数,会返回一个协程对象。因此,协程必须显式启动,仅仅创建协程并不能启动它。协程可以在函数体的不同位置暂停和恢复。这种暂停和恢复能力使得异步行为成为可能!

await 语句只能出现在协程函数中,await 只能用于实现了 __await__() 方法的可等待对象,包括协程对象(coroutines)、任务(Task)、未来对象(Future)。

刚才提到的可等待对象是什么呢?

协程对象 : 调用 async def 函数返回的对象;

任务 Task: 是绑定到事件循环的协程,任务还维护一个回调函数列表;

未来对象Future:是一个用来表示状态和结果的状态对象,Task也是通过继承此类获得的状态管理;

await 和 控制权

并不是每个await都会把控制权交回事件循环,await 任务Future对象状态是交回事件循环的。await 一个协程对象,相当于同步执行,直接用当前的协程运行,并等待其完成。

例1:await 一个coroutine协程

代码语言:javascript
复制
async def plant_a_tree():
    await dig_the_hole()

        # 与植树相关的其他指令。
    ...

这种情况下,await 不会交回控制权,会直接在当前协程运行dig_the_hole协程,并等待其退出,是同步等待。

例2:await 一个 task任务

代码语言:javascript
复制
async def plant_a_tree():
    dig_the_hole_task = asyncio.create_task(dig_the_hole())

    # 添加其他任务 ...
    

    # 必须等待的时候等待其完成
    await dig_the_hole_task

    # 与植树相关的其他指令。
    ...

这种情况下,create_task() 创建任务并将其加入事件循环,此时不会运行,await 会把当前函数添加到dig_the_hole_task任务的回调中,然后控制权交回事件循环,事件循环就会调度dig_the_hole_task,等dig_the_hole_task运行完, await 才会返回 。

注:本例只有一个任务,看上去好像await任务时才会执行,但不是这样的。create_task()时任务就已经加入事件循环了,之后对任何任务的await都会把控制权交回事件循环,事件循环中的任务都有可能被执行。并不是等await某个任务时才会执行这个任务。

例3:等待Future对象

代码语言:javascript
复制
import asyncio

# await future 时,实际上是在等待回调机制
async def set_result_after_delay(future):
    print("设置 Future 结果...")
    await asyncio.sleep(1)
    future.set_result("最终结果")

async def main():
    future = asyncio.Future()

    # 启动设置结果的任务
    asyncio.create_task(set_result_after_delay(future))

    print("开始 await future...")
    result = await future  # 这里会等待回调系统通知完成
    print(f"await 完成,得到结果: {result}")

asyncio.run(main())

这个例子中首先创建了设置future对象结果的协程,添加到事件循环,等运行到await future时,控制权交回事件循环,set_result_after_delay 开始执行,并设置了future的结果,之后 await future 就返回了。

理清协程和yield的关系

1.从协程中有效地放弃控制权:

从协程中“yield”,方法是 await 一个在其 __await__ 方法中 yield 的对象。

当然,更推荐 await asyncio.sleep(0)

代码语言:javascript
复制
class YieldToEventLoop:
    def __await__(self):
        yield
        
async def somefunc():
    await YieldToEventLoop()
    await asyncio.sleep(0)   # 这个更好

2.使用 yield from 定义协程(旧式协程,不要再用了):

代码语言:javascript
复制
import asyncio

@asyncio.coroutine
def old_style_coroutine():
    yield from asyncio.sleep(1)
    return "done"

带有 yield 的协程函数,叫异步生成器:

代码语言:javascript
复制
import asyncio

async def async_data_generator():
    for i in range(3):
        await asyncio.sleep(0.1)  # 模拟异步操作
        yield i

async def main():
    # 必须使用 async for 来迭代
    async for item in async_data_generator():
        print(item)

# asyncio.run(main())
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2025-11-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 漫跑的小兔 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 协程运行步骤:
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档