从零开始写 MCP SDK ,急切任务(立即执行任务模式)工厂

268 阅读5分钟

急切任务工厂(eager task factories)是一种创建和启动异步任务的机制,与更常见的“惰性任务创建”方式有所不同。理解 eager task factories 的关键在于对比这两种方式的任务调度执行时机。

image.png

更多相关视频

惰性任务创建 (通常的 asyncio 行为)

当你使用 asyncio.create_task(coro()) 或类似的函数创建一个任务时,你实际上是创建了一个 Task 对象,但这个任务不会立即开始执行。这个新创建的任务会被提交到 asyncio 的事件循环中,事件循环会在下一次迭代时安排执行这个任务。这意味着在创建任务的代码和任务真正开始运行之间通常会有一个“等待”的阶段,至少需要等待事件循环的下一次调度。这种行为被称为“惰性”,因为任务的执行被推迟到了事件循环的后续迭代。

急切任务工厂 (experimental asyncio feature)

“急切任务工厂”的概念意味着,当使用这种机制创建任务时,任务可能会立即开始执行,而不需要等待事件循环的下一次迭代。这打破了 asyncio 中通常的任务调度语义。新创建的任务可能会在创建它的同一个事件循环迭代中就开始运行,甚至可能会抢占当前正在运行的其他协程。文档中提到这种行为是“实验性的”,因为它可能会导致一些非预期的行为,尤其是在那些假设了任务是按照标准 asyncio 调度方式执行的代码中。

使用急切任务可能潜在风险

  • 依赖执行顺序的代码: 如果你的代码依赖于任务创建的顺序和任务开始执行的顺序之间的关系(例如,假设任务 B 在任务 A 创建后才开始执行),那么急切任务工厂可能会打乱这种预期的顺序。
  • 资源竞争: 如果多个任务在创建后立即开始竞争共享资源,而你没有考虑到这种立即执行的可能性,可能会导致意想不到的并发问题。
  • 调试和跟踪: 由于任务的启动时机更加难以预测,使用急切任务工厂可能会使调试和跟踪异步代码变得更加困难。

急切任务工厂优点

虽然是实验性的,但引入“急切任务工厂”可能有以下一些潜在的动机:

  • 某些特定的并发模式: 在某些特定的并发模式下,立即执行新创建的任务可能更符合逻辑或能带来性能上的优势。
  • 简化某些同步原语的实现: 某些底层的同步原语或并发控制机制可能更容易在急切执行的环境中实现。
  • 与其他并发模型的兼容性: 某些其他的并发模型(例如某些 Actor 模型)可能更倾向于立即执行新创建的“Actor”或“进程”。

这里给出实例介绍如何临时更改模式为立即执行任务

@contextmanager
def _get_loop():
    # careful using this, you should NOT async yield within the context
    loop = asyncio.get_running_loop()

    if sys.version_info.minor < 12:
        yield loop
        return

    # temporarily override default task factory as eager
    task_factory_bak = loop.get_task_factory()
    loop.set_task_factory(asyncio.eager_task_factory)  # type: ignore
    try:
        yield loop
    finally:
        loop.set_task_factory(task_factory_bak)


def create_task(coro: Coroutine):
    with _get_loop() as loop:
        return loop.create_task(coro)

简单解释一下上面的代码 @contextmanager: 这是一个装饰器,用于将一个生成器函数转换为上下文管理器。_get_loop(): 这是一个生成器函数,用作上下文管理器。其目的是获取当前正在运行的 asyncio 事件循环。 # careful using this, you should NOT async yield within the context: 注释警告在这个上下文管理器内部不应该使用 async yield。这通常是因为上下文管理器的 __enter____exit__ 方法应该是同步的。

loop = asyncio.get_running_loop(): 获取当前线程中正在运行的事件循环。如果当前没有运行的事件循环,它会抛出一个 RuntimeErrorif sys.version_info.minor < 12:: 检查 Python 的次版本号是否小于 12。yield loop: 如果 Python 版本小于 3.12,则直接产生获取到的事件循环。

接下来重点来了,在 Python 3.12 及更高版本中, task_factory_bak = loop.get_task_factory() 获取当前事件循环的任务工厂。任务工厂是用于创建任务的可调用对象。也就是可以临时可以将懒加载变成立即加载 loop.set_task_factory(asyncio.eager_task_factory) 临时将事件循环的任务工厂设置为 asyncio.eager_task_factoryeager_task_factory 是在 Python 3.12 中引入的,创建的任务会在创建后立即尝试执行,而不是等待事件循环的下一次迭代。 try...finally: 确保在退出上下文管理器时恢复原来的任务工厂。 yield loop: 产生当前(临时修改过的)事件循环。 loop.set_task_factory(task_factory_bak): 在 finally 块中,将事件循环的任务工厂恢复为之前保存的值。

总结:

急切任务工厂是一种实验性的异步任务创建机制,它可能导致新创建的任务在创建后立即开始执行,而不需要等待事件循环的下一次迭代。这与 asyncio 通常的惰性任务创建行为不同,可能会导致依赖标准 asyncio 调度语义的代码出现意外行为。因此,在使用这种特性时需要格外小心,并充分理解其潜在的影响。文档中明确指出它不遵循通常的任务调度语义,建议在没有充分理解其含义的情况下避免使用。