在Python中并发控制生成器和子例程

时间:2013-06-04 21:45:19

标签: python concurrency generator coroutine

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

功能bigopreport的每个主要步骤调用data,然后调用status。复制data用于每个步骤(或使其持久)会很昂贵,因此report必须在bigop继续之前的每一步完成。

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

假设无法更改函数bigopview

问题:如何定义genreport和程序main以使bigopinit上运行,以及值的可视化状态报告将显示 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

1 个答案:

答案 0 :(得分: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 yield,就可以写send view([data])个新对象。或者,更简单地说,只需重复调用bigop并让它迭代一个对象。

无论如何,我看不出你对此有何帮助。 bigop不是协程,你不能把它变成一个协程。鉴于此,没有办法强迫它与其他协同程序合作共享。

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

如果你只想交错处理和报告而不用并发 - 或定期挂钩到bigop或其他类似的东西 - 你可以做一个coroutine,但它与使用子程序具有完全相同的效果 - 上面的两个例子非常相同。所以,你只是无缘无故地增加了复杂性。

(如果gevent是I / O绑定的,您可以使用greenlet,并将I / O操作monkeypatch以将它们异步化,如eventletviewdriver。但是如果它是CPU这样做没有任何好处。)


阐述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__遍历循环时,它会调用我们的bigop,从队列中弹出一个元素。如果view保证比view更快,那么一切都会有效,但第一次IndexError提前,它会得到__next__

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

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

因此,您必须在迭代下添加另一级协同程序。然后,next_coro可以等待next(通过调用call_coro),这会产生__call__,然后产生它获得的值。同时,send必须call_coro到同一next_coro,等待它,然后屈服。

到目前为止,这并没有改变任何东西,因为你有两个例程都试图驱动__next__,其中一个(next)没有阻止其他地方,所以它是只是旋转它send(None)来自__call__的{​​{1}}看起来像next_coro

解决这个问题的唯一方法是建立一个蹦床(PEP 342包括一个通用蹦床的来源,虽然在这种情况下你可以建立一个更简单的专用蹦床),安排call_coronext_coro明确地替换,确保run正确处理两个不同入口点之间的交替,然后从__next__(和__init__)驱动调度程序的{{1}}。

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

由于你的原始问题是否比使用线程更简单,答案是:不,没有。