正确使用asyncio.Condition的wait_for()方法

时间:2019-08-29 10:24:24

标签: python python-3.x python-asyncio

我正在使用Python的asyncio模块编写一个项目,并且我想使用其同步原语来同步我的任务。但是,它的行为似乎不符合我的预期。

从文档中看来,Condition.wait_for()提供了一种方法,允许协程等待特定的用户定义条件评估为true。但是,在尝试使用该方法时,它的行为似乎是我无法预期的-我的条件仅被检查了一次,并且如果发现它是假的,等待的任务将永远挂起,而无需再次检查。我在下面写了一个简短的例子来说明我要做什么:

#!/usr/bin/env python

import asyncio

thing = False

setter_done = None
getter_done = None

async def main():

    setter_done = asyncio.Event()
    getter_done = asyncio.Event()

    setter = asyncio.ensure_future(set_thing())
    getter = asyncio.ensure_future(get_thing())

    #To avoid the loop exiting prematurely:
    await setter_done.wait()
    await getter_done.wait()

async def set_thing():

    global thing
    global setter_done

    thing = False
    #sleep for some arbitrary amount of time; simulate work happening
    await asyncio.sleep(10)
    thing = True

    print("Thing was set to True!")
    setter_done.set()

async def get_thing():

    global thing
    global getter_done

    def check_thing():
        print("Checking...")
        return thing

    c = asyncio.Condition()
    await c.acquire()
    await c.wait_for(check_thing)
    c.release()
    print("Thing was found to be true!")
    getter_done.set()


if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())

我希望它会打印如下内容:

Checking...
Thing was set to True!
Checking...
Thing was found to be True!

相反,我得到:

Checking...
Thing was set to True!
... (hangs indefinitely)

2 个答案:

答案 0 :(得分:3)

我发布了一个带有很多评论的完整答案,以帮助解决类似问题的人。我已将代码示例更改为使用类而不是全局变量。有点长,但我希望它不会太复杂。

基本上 Command 类代表一个任务。它是异步的,所以它可以做很多事情。在我的例子中,我只创建了两个虚拟命令(读取“两个任务”),一个暂停 5 秒,一个暂停 8 秒,然后我等待它们都结束并出现条件。显然,条件不是做我所做的事情的唯一方法,但为了与原始答案保持一致,我认为提供一个完整的示例很有趣。就这样吧!

import asyncio
from typing import Set

class Command:

    """A command, an asynchronous task, imagine an asynchronous action."""

    async def run(self):
        """To be defined in sub-classes."""
        pass

    async def start(self, condition: asyncio.Condition,
            commands: Set['Command']):
        """
        Start the task, calling run asynchronously.

        This method also keeps track of the running commands.

        """
        commands.add(self)
        await self.run()
        commands.remove(self)

        # At this point, we should ask the condition to update
        # as the number of running commands might have reached 0.
        async with condition:
            condition.notify()

class Look(Command):

    """A subclass of a command, running a dummy task."""

    async def run(self):
        print("Before looking...")
        await asyncio.sleep(5)
        print("After looking")

class Scan(Command):

    """A subclass of a command, running a dummy task."""

    async def run(self):
        print("Before scanning...")
        await asyncio.sleep(8)
        print("After scanning")

async def main():
    """Our main coroutine, starting commands."""
    condition = asyncio.Condition()
    commands = set()
    commands.add(Look())
    commands.add(Scan())
    asyncio.gather(*(cmd.start(condition, commands) for cmd in commands))

    # Wait for the number of commands to reach 0
    async with condition:
        await condition.wait_for(lambda: len(commands) == 0)
        print("There's no running command now, exiting.")

asyncio.run(main())

所以在实践中(像往常一样,从头开始),我们将 main 称为协程。在 main 中,我们创建了两个命令 LookScan,并调用它们的 start 方法。 start 方法定义在每个命令上,它基本上负责在命令运行前将命令本身写入集合,并在运行后(即完全完成后)将其删除。然后它应该通知条件再次检查命令的长度。当没有命令剩下时,程序结束。如果您运行此脚本(我使用 Python 3.8 运行它),您应该看到如下内容:

Before scanning...
Before looking...
After looking
After scanning
There's no running command now, exiting.

请注意,这两个命令同时开始(好吧,Look 开始的时间稍早,事实上,但 Scan 开始于 Look 完成之前)。但是 Look 确实在 Scan 之前结束(大约 3 秒)。直到两个命令都完成后,我们的条件才会被检查。

可以使用事件、锁或信号量来代替吗?可能,但我喜欢在那个例子中使用一个条件。您无需进行大量修改即可轻松完成更多任务。

答案 1 :(得分:2)

将事情设置为true后,您需要添加c.notify_all()。条件只有在被“通知”后才检查其谓词,否则不会发生任何事情。