Python中的自定义协同程序

时间:2018-02-10 18:33:59

标签: python asynchronous

我想实现并运行自定义Python协同程序(不使用asyncio),以便更好地理解异步机制。

当第一个任务正在等待,什么都不做时,我被期望能够使用并发来启动第二个任务。

这里是堆栈器的同步实现(这是一个任意用例)。

def log(*msg):
    print(int(time() - start), ':', *msg)

def stack(stack, item):
    sleep(1)
    stack.append(item)

start = time()
words = []
stack(words, 'hello')
log(words)
stack(words, 'world')
log(words)

这是输出,正如我所料:

1 : ['hello']
2 : ['hello', 'world']

然后尝试异步实现相同的堆栈器。

def coroutine(func):
    def starter(*args, **kwargs):
        gen = func(*args, **kwargs)
        next(gen)
        return gen
    return starter

@coroutine
def a_sleep(count):
    while True:
        yield
        sleep(count)

@coroutine
def a_stack(stack):
    while True:
        item = yield
        yield from a_sleep(1)
        stack.append(item)

start = time()
words = []
a_stack(words).send('hello')
log(words)
a_stack(words).send('world')
log(words)

# Wait all tasks to finish
sleep(4)
log(words)

预期产出:

0 : []
1 : ['hello', 'world']
5 : ['hello', 'world']

实际输出:

1 : []
2 : []
6 : []

我认为我错过了重要的事情。我希望我的方法是相关的。

使用其他日志,我注意到a_stack函数从不执行追加部分。

2 个答案:

答案 0 :(得分:1)

您的问题是您的生成器暂停在yield函数中的a_sleep()表达式(通过yield from a_sleep(1))委派。那里的生成器还有无限,所以永远不会返回。您永远无法将您的发电机推进到足以达到stack.append(item)来电。

我认为你误解了yield from在这里的作用。 yield from将发电机的控制权移到另一台发电机上;其他生成器必须在yield from表达式完成并返回之前完成迭代

>>> @coroutine
... def a_sleep(count):
...     while True:
...         yield 'sleeping'  # to illustrate where we are stuck
...         sleep(count)
...
>>> words = []
>>> g = a_stack(words)
>>> g.send('hello')
'sleeping'
>>> g.send('hello')
'sleeping'
>>> g.send('hello')
'sleeping'

而不是使用sleep()和无限循环,记录时间,并循环直到时间过去:

>>> @coroutine
... def a_sleep(count):
...     start = time()
...     while int(time() - start) < count:
...         yield 'sleeping'
...
>>> g = a_stack(words)
>>> g.send('hello')
'sleeping'
>>> g.send('hello')
'sleeping'
>>> g.send('hello')
>>> words
['hello']

你必须继续迭代你的生成器(在循环中,也许?)让它们交替执行。

asyncio.sleep() function当然比这更有效率;它使用附加到事件循环提供的Future() objectAbstactEventLoop.call_later() functionality。循环让未来的对象知道时间到了什么时候,此时未来被标记为“准备好”&#39;产生它的协程再次继续。

答案 1 :(得分:1)

该行:

a_stack(words).send('hello')

做两件事:

  1. 创建并运行新生成器
  2. 将字符串hello发送到生成器
  3. 创建的生成器等待项目到达,然后一旦恢复,对项目执行某些操作。这就是问题,你永远不会恢复生成器,你扔掉它并创建一个新的生成器,并继续以相同的方式使用。要修复它,您的发送代码应该执行以下操作:

    coro = a_stack(words)
    coro.send('hello')
    log(words)
    coro.send('world')
    log(words)
    

    但还有另一个问题。在实际附加到堆栈之前,a_stack将其执行推迟到另一个迭代器,永远不会停止产生。解决问题的代码a_sleep的一种方法是:

    @coroutine
    def a_sleep(count):
        t0 = time()
        while time() - t0 < count:
            yield 'notyet'
    

    然后,您需要一个调度程序或至少一个更具弹性的send版本,它实际上可以处理延迟执行的任务。一个简单(非常低效)的可能是这样的:

    def sync_send(c, v):
        while True:
            ret = c.send(v)
            if ret != 'notyet':
                return ret
    

    coro.send('hello')替换sync_send(coro, 'hello')后,会显示预期的输出。

    真正的调度程序永远不会繁忙循环;它将由sleep和其他可能的阻塞调用指示,例如从文件或套接字读取,它必须等待哪些IO /定时事件。在适当的事件到达后,它将唤醒正确的任务。这是asyncio所做的核心。

    要了解有关生成器和yield from如何用作异步编程的核心抽象的更多信息,我推荐Dave Beazley的精彩讲座Python Concurrency From the Ground。在讲座中,Dave在现场观众面前实施了一个协程调度程序,展示了他的curio库的设计。