【问题标题】:Concurrent control of generator and subroutine in PythonPython中生成器和子程序的并发控制
【发布时间】:2013-05-31 10:50:12
【问题描述】:

函数bigop(init, report) 对派生自init 的大型动态内部数据结构data 进行操作,并接受可调用的report(data)。函数status(data)返回data当前状态的摘要。

函数bigop 在每个主要步骤调用reportdata 的当前状态,这反过来又调用status。为每个步骤复制data(或使其持久化)会很昂贵,因此report 必须在bigop 继续之前完成每个步骤。

函数view(gen) 接受生成器gen 生成状态摘要的连续值,并在生成时显示每个值的可视化。函数view 维护依赖于迄今为止生成的值的内部状态。 (在我的特殊情况下,这种内部状态可以被复制,但最好避免。)

假设函数bigopview 不能更改。

问题:如何定义genreport 和一个程序main,使得bigop 将在init 上运行,并显示状态报告值的可视化 bigop 达到每个主要步骤?

难点在于reportgen是在其他函数内部调用的,所以通常的Python协程模式是不适用的。 (在我的特殊情况下,bigop 实际上是一个生成器。)

A previous question关于使用回调从普通函数生成生成器的问题是使用线程回答的,但我想知道是否有更简单的方法。

注意:只有与 Python 2.7 兼容的答案对我有用;但如果这些差异是相关的,我很想看看 Python 3 的答案。

def bigop(init, report):
    data = init
    while data < 10:           # complicated condition
        print 'working ...'
        data += 1              # complicated operation
        report(data)

def view(gen):
    for value in gen:
        print value            # complicated display routine

def main(init):
    """
    example:

    >> main(7)
    'working ...'
    8
    'working ...'
    9
    'working ...'
    10
    """
    pass

问题:main如何定义?

【问题讨论】:

  • 很难从这些描述中理解你的设计。你能写一个SSCCE,用简单的例子来说明你在做什么,这样你就可以指着它说,“每次这条线运行时,我希望这里的代码能做到这一点”?

标签: python concurrency generator coroutine


【解决方案1】:

鉴于您的示例代码:

def main(init):
    def report(x):
        print x
    bigop(init, report)

但是,我认为这不是您想要的。大概您希望report 以某种方式将数据输入view

您可以通过扭转局面来做到这一点——view 不是驱动另一个生成器的生成器,而是由外部调用者调用send 驱动的生成器。例如:

def view():
    while True:
        value = yield
        print value
def main(init):
    v = view()
    v.next()
    def report(x):
        v.send(x)
    bigop(init, report)

但是你说view不能改。当然,你可以写一个viewdriver 来表示yields 一个新对象,只要你send 它一个。或者,更简单地说,只是重复调用 view([data]) 并让它遍历单个对象。

无论如何,我看不出你期望这有什么帮助。 bigop 不是协程,你不能把它变成一个协程。鉴于此,没有办法强制它与其他协程合作共享。

如果您想同时交错处理和报告,您必须使用线程(或进程)。 “REPORT 必须在 BIGOP 继续之前的每个步骤完成”这一事实已经是您要求的一部分,这意味着您无论如何都不能安全地在这里并发任何事情,所以我不确定您在寻找什么。

如果您只是想在没有并发的情况下交错处理和报告,或者定期挂接到bigop,或其他类似的事情-您可以使用协程来做到这一点,但它与使用子程序的效果完全相同——上面的两个例子几乎是等价的。所以,你只是无缘无故地增加了复杂性。

(如果bigop 是 I/O 绑定的,您可以使用 greenlets,并猴子补丁 I/O 操作来异步化它们,就像 geventeventlet 所做的那样。但如果它是 CPU 绑定的,就会有这样做没有任何好处。)


详细说明viewdriver 的想法:我上面描述的内容相当于每次都调用view([data]),所以它对你没有帮助。如果你想让它成为一个迭代器,你可以,但它只会导致阻塞 bigop 或旋转 view,因为你试图用消费者来喂养消费者。

作为生成器可能很难理解,所以我们将它构建为一个类:

class Reporter(object):
    def __init__(self):
        self.data_queue = []
        self.viewer = view(self)
    def __call__(self, data):
        self.data_queue.append(data)
    def __iter__(self):
        return self
    def __next__(self):
        return self.data_queue.pop()

bigop(init, Reporter())

每次bigop 调用report(data),它调用我们的__call__,向我们的队列添加一个新元素。每次view 循环时,它都会调用我们的__next__,从队列中弹出一个元素。如果保证bigopview 快,一切都会正常,但view 第一次领先时,它会得到IndexError

解决这个问题的唯一方法是让__next__ 尝试直到data_queue 不为空。但只是这样做将永远旋转,而不是让bigop 完成工作以产生新元素。而且你不能将__next__ 变成一个生成器,因为view 期望的是一个值的迭代器,而不是迭代器的迭代器。

幸运的是,__call__ 可以是一个生成器,因为bigop 并不关心它返回的值是多少。所以,你可以扭转局面。但你不能那样做,因为那样就没有什么东西可以驱动那台发电机了。

因此,您必须在迭代下方添加另一个级别的协程。然后,__next__ 可以等待next_coro(通过在其上调用next),它产生一个call_coro,然后产生它得到的值。同时,__call__ 必须 send 到相同的 call_coro,等待它,然后让步。

到目前为止,这并没有改变任何东西,因为你有两个例程都试图驱动 next_coro,其中一个 (__next__) 没有阻塞其他任何地方,所以它只会旋转——next 调用看起来像来自__call__send(None)

解决此问题的唯一方法是构建蹦床(PEP 342 包括通用蹦床的源代码,尽管在这种情况下您可以构建更简单的专用蹦床),安排 next_corocall_coro要显式交替,请确保next_coro 正确处理两个不同入口点之间的交替,然后从__next__(和__init__)驱动调度程序的run

困惑?在本周的剧集之后,你不会是……不,我在开谁的玩笑。你会感到困惑。写下所有这些是一回事。调试它是另一个。 (特别是因为每个重要的堆栈跟踪都立即在蹦床上终止。)所有这些工作对你有什么好处?与使用 greenlets 或线程完全相同的好处,也有完全相同的缺点。

由于您最初的问题是是否有比使用线程更简单的方法,所以答案是:不,没有。

【讨论】:

  • 感谢您的分析。我的简短总结是:不,在 Python 中没有比使用线程更简单的方法了。我不太明白你对viewdriver 的意思,但使用单独调用view([data]) 的问题是view 也有内部状态(我已经编辑了问题以反映这一点)。
  • 如果bigopview 的定义尚未修复,您的其他建议肯定会起作用,但我想知道是否有办法有效地包装gen 以便view仅在生成新值时才继续。
  • 让我编辑答案以解释更多关于 viewdriver 想法的信息……但我认为这对您没有帮助,您的简短总结是正确的。您有一个组件希望位于推链上,另一个组件希望位于拉链上,您也无法更改。
猜你喜欢
  • 2019-11-07
  • 1970-01-01
  • 2014-07-05
  • 2012-12-25
  • 2014-12-27
  • 2011-08-22
  • 2011-01-19
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多