使用“yield”进行上下文切换

5
我正在阅读一个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个回答

5
用于此目的的生成器通常被称为“任务”(在许多其他术语中),为了清晰起见,我将在此处使用该术语。是的,这是可能的。实际上,有几种方法可以在某些情况下工作并且有意义。但是,没有任何一种方法(据我所知)可以在不具有至少一个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的头脑风暴。尽管网络基于档案不适合阅读半年前写的数十个线程中的数百个邮件,但这些邮件线程也可能对您有所帮助。)


非常有趣。有很多我不知道的东西。那个教程看起来也不错。干杯。 - kaiseroskilo

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

没问题。谢谢你的演示。我在想是否可能让生成器A产生next(generatorB)(并且生成器B产生next(generatorA),以此类推)。 - kaiseroskilo
2
问题有三个方面:第一个是我不知道生成器如何产生自身,这对于设置相互递归是必要的。第二个是在评估yield语句之前,需要调用next/0才能返回,这将导致尝试重新进入尚未产生的生成器。第三个是在没有尾调用消除的语言中进行相互递归,因此如果生成器产生太多次,最终会耗尽堆栈。 - Recurse

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接