使用'yield'进行上下文切换

5 投票
2 回答
2878 浏览
提问于 2025-04-17 15:40

我在看一个关于gevent的教程时,看到了一段有趣的代码:

import gevent

def foo():
    print('Running in foo')
    gevent.sleep(0)
    print('Explicit context switch to foo again')

def bar():
    print('Explicit context to bar')
    gevent.sleep(0)
    print('Implicit context switch back to bar')

gevent.joinall([
    gevent.spawn(foo),
    gevent.spawn(bar),
])

在这段代码中,执行的顺序是这样的:foo -> bar -> foo -> bar。 我想知道,能不能不使用gevent模块,而是用yield语句来实现同样的效果? 我试着用'yield'来做,但不知道为什么就是不行... :(

2 个回答

2

关键是要明白,你需要自己提供一个循环来驱动程序——我在下面提供了一个简单的示例。我有点懒,使用了一个队列对象来实现先进先出(FIFO),因为我已经有一段时间没用Python做过大型项目了。

#!/usr/bin/python

import Queue

def foo():
    print('Constructing foo')
    yield
    print('Running in foo')
    yield
    print('Explicit context switch to foo again')

def bar():
    print('Constructing bar')
    yield
    print('Explicit context to bar')
    yield
    print('Implicit context switch back to bar')

def trampoline(taskq):
    while not taskq.empty():
        task = taskq.get()
        try:
            task.next()
            taskq.put(task)
        except StopIteration:
            pass

tasks = Queue.Queue()
tasks.put(foo())
tasks.put(bar())

trampoline(tasks)

print('Finished')

然后运行这个代码:

$ ./coroutines.py 
Constructing foo
Constructing bar
Running in foo
Explicit context to bar
Explicit context switch to foo again
Implicit context switch back to bar
Finished
6

为了这个目的使用的生成器通常被称为 任务(还有很多其他的称呼),为了清晰起见,我在这里使用这个词。是的,这是可能的。实际上,有几种方法可以实现,并且在某些情况下是有意义的。然而,至少在我所知道的情况下,没有一种方法可以在没有 gevent.spawngevent.joinall 中至少一个的等效物的情况下工作。更强大且设计良好的方法需要两个都有。

根本问题是这样的:生成器可以被挂起(当它们遇到 yield 时),但就仅此而已。要重新启动它们,你需要其他代码调用 next()。实际上,你甚至需要在新创建的生成器上调用 next(),它才能开始做 任何事情。同样,生成器本身也不是决定下一个应该运行什么的最佳地方。因此,你需要一个循环来启动每个任务的时间片(运行它们直到下一个 yield),并在它们之间不断切换。这通常被称为调度器。调度器通常会变得非常复杂,所以我不会尝试在一个回答中写一个完整的调度器。不过,有一些核心概念我可以尝试解释:

  • 通常将 yield 看作是将控制权交还给调度器(实际上类似于你代码中的 gevent.sleep(0))。这意味着,生成器可以做它想做的事情,当它处于一个方便且可能有用的上下文切换位置时,它就会 yield
  • 在 Python 3.3 及以上版本中, yield from 是一个非常有用的工具,可以委托给另一个生成器。如果你不能使用它,你就得让调度器模拟一个调用栈,并将返回值路由到正确的位置,并在你的任务中做类似 result = yield subtasks() 的事情。这种方式更慢,实施起来更复杂,而且不太可能产生有用的堆栈跟踪(yield from 可以免费做到这一点)。但直到最近,这还是我们能用的最好方法。
  • 根据你的使用场景,你可能需要一系列工具来管理任务。常见的例子包括生成更多任务、等待一个任务完成、等待多个任务中的任何一个完成、检测其他任务的失败(未捕获的异常)等。这些通常由调度器处理,任务则提供一个 API 与调度器进行通信。一种不错(但并不总是完美)的方法是通过 yield 特殊值来进行这种通信。
  • 生成器基础的任务和 gevent(以及类似库)之间一个相当重要的区别是,后者的上下文切换是隐式的,而任务则很容易识别上下文切换:只有 yield [from] 的东西才可能运行调度器代码。例如,你可以仅通过查看代码来确保一段代码是原子性的(相对于其他任务;如果你加入了线程,你还得独立考虑它们),而不需要检查它调用的 任何东西

最后,你可能会对 Greg Ewing 的 教程 感兴趣,关于如何创建这样的调度器。(这个问题出现在 python-ideas 上,当时我们在头脑风暴现在的 PEP 3156。这些邮件线程可能也会引起你的兴趣,尽管基于网络的档案并不太适合阅读几个月前数十个线程中的数百封邮件。)

撰写回答