【问题标题】:context switching with 'yield'使用 'yield' 进行上下文切换
【发布时间】:2013-02-12 20:44:32
【问题描述】:

我在阅读 gevent 教程时看到了这个有趣的 sn-p:

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'来做到这一点,但由于某种原因我无法让它工作...... :(

【问题讨论】:

    标签: python gevent


    【解决方案1】:

    用于此目的的生成器通常称为任务(在许多其他术语中),为了清楚起见,我将在此处使用该术语。是的,有可能。事实上,有几种方法在某些情况下有效且有意义。但是,如果没有gevent.spawngevent.joinall 中的至少一个的等效项,则没有一个(据我所知)。更强大和设计良好的需要两者的等效项。

    根本问题是这样的:生成器可以暂停(当它们命中yield 时),但仅此而已。要再次启动它们,您需要在它们上调用 next() 的其他代码。实际上,您甚至需要在新创建的生成器上调用 next() 才能开始执行任何操作。 同样,生成器本身也不是决定接下来应该运行什么的最佳位置。所以你需要一个循环来启动每个任务的时间片(将它们运行到下一个yield)并无限期地在它们之间切换。这通常称为调度程序。它们往往很快就会变得非常多毛,所以我不会尝试在一个答案中编写完整的调度程序。不过,我可以尝试解释一些核心概念:

    • 通常将yield 视为将控制权交还给调度程序(实际上类似于代码中的gevent.sleep(0))。这意味着,生成器会做它想做的任何事情,当它处于上下文切换方便且可能有用的地方时,它yields。
    • 在 Python 3.3+ 中,yield from 是一个非常有用的工具,可以委托给另一个生成器。如果您不能使用它,您必须让调度程序模拟调用堆栈并将返回值路由到正确的位置,并在您的任务中执行result = yield subtasks() 之类的操作。这更慢,实现起来更复杂,并且不太可能产生有用的堆栈跟踪(yield from 这样做是免费的)。但直到最近,它还是我们拥有的最好的。
    • 根据您的使用案例,您可能需要多种工具来管理任务。常见的例子是产生更多的任务,等待一个任务完成,等待几个任务中的任何一个完成,检测其他任务的失败(未捕获的异常)等。这些通常由调度程序处理,任务被赋予一个 API与调度程序通信。进行这种交流的一种简洁(但并不总是完美)的方式是yielding 特殊值。
    • 基于生成器的任务和 gevent(以及类似的库)之间的一个相当重要的区别是后者中的上下文切换是隐式的,而任务使得识别上下文切换变得微不足道:只有 yield [from] 可以运行调度程序代码.例如,您可以通过查看代码来确定一段代码是否是原子的(w.r.t. 其他任务;如果您将线程添加到混合中,您必须独立担心它们),而无需检查 任何内容 em> 它调用。

    最后,您可能会对 Greg Ewing 的tutorial 感兴趣,了解如何创建这样的调度程序。 (这出现在python-ideas 上,当时正在对现在的 PEP 3156 进行头脑风暴。这些邮件线程也可能对您感兴趣,尽管基于 Web 的存档并不真正适合阅读数十个线程中的数百封邮件一年前。)

    【讨论】:

    • 非常有趣。很多我不知道的事情。那个教程看起来也不错。干杯
    【解决方案2】:

    关键是要意识到您必须提供自己的驾驶循环——我在下面提供了一个简单的演示。我很懒,用一个Queue对象来提供一个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
    

    【讨论】:

    • 有道理。也感谢演示。我想知道 generatorA 是否有可能产生 next(generatorB) (和 generatorB 产生 next(generatorA) 等等)。
    • 这个问题有三个方面:首先是我不知道生成器有什么方法可以让自己产生,这是设置相互递归所必需的。第二个是对 next/0 的调用需要在 yield 语句被评估之前返回,这将导致尝试重新进入尚未产生的生成器。第三,这是一种没有尾调用消除的语言中的相互递归,因此如果生成器产生太多次,您最终会耗尽堆栈。
    猜你喜欢
    • 2016-03-31
    • 2011-07-23
    • 1970-01-01
    • 2018-09-22
    • 2017-09-09
    • 2011-07-27
    • 2011-08-13
    • 1970-01-01
    相关资源
    最近更新 更多