重新启动异步协程,而无需等待其他协程完成

时间:2019-04-22 06:03:56

标签: asynchronous async-await python-asyncio python-3.7

我正在使用以下问题在Python中进行异步编程:

  

模拟多个人在同一个食物碗中进食一定数量的食物。每个人一次可以食用x份食物,然后将其咀嚼y秒钟(模拟了通话阻塞)。只要碗里还有食物,一个人就可以独立于他人咀嚼食物。

     

为每个食者和饭碗定义班级。最终目标是在食品碗类中具有一个功能,该功能可以接受一系列人员,并让他们从碗中开始进食,直到碗空为止。每当有人从碗里拿食物时,都应该在stdout上打印一条消息。

例如,如果我有一个盛有25份食物的食物碗,并且三个人分别是A,B和C:

  • A一次吃2份食物,并咀嚼3秒钟
  • B一次吃3份食物,并咀嚼4秒钟
  • C一次吃5份食物,并咀嚼2秒钟

因此,预期输出(打印到标准输出)应该是:

(t=0) Person A takes 2 servings of food, leaving 23 servings in the bowl.
(t=0) Person B takes 3 servings of food, leaving 20 servings in the bowl.
(t=0) Person C takes 5 servings of food, leaving 15 servings in the bowl.
(t=2) Person C takes 5 servings of food, leaving 10 servings in the bowl.
(t=3) Person A takes 2 servings of food, leaving 8 servings in the bowl.
(t=4) Person B takes 3 servings of food, leaving 5 servings in the bowl.
(t=4) Person C takes 5 servings of food, leaving 0 servings in the bowl.
(t=4) The bowl is empty!

(在t=4之类的两个人准备再吃另一份的时候,顺序并不重要) 该代码是我的尝试:

import asyncio
import time


class Person():
    def __init__(self, name, serving_size, time_to_eat):
        self.name = name
        self.serving_size = serving_size
        self.time_to_eat = time_to_eat

    async def eat_from(self, foodbowl):
        servings_taken = self.serving_size if foodbowl.qty >= self.serving_size else foodbowl.qty
        foodbowl.qty -= servings_taken
        t = round(time.time() - foodbowl.start_time)
        print("(t={}) Person {} picks up {} servings of food, leaving {} servings in the bowl.".format(t, self.name, servings_taken, foodbowl.qty))
        await asyncio.sleep(self.time_to_eat)
        return servings_taken


class FoodBowl():
    def __init__(self, qty):
        self.qty = qty

    async def assign_eaters(self, eaters):
        self.start_time = time.time()
        while self.qty > 0:
            await asyncio.gather(*[eater.eat_from(self) for eater in eaters])
        t = round(time.time() - self.start_time)
        print("The bowl is empty!")


bowl = FoodBowl(25)
person_1 = Person("A", 2, 3)
person_2 = Person("B", 3, 4)
person_3 = Person("C", 5, 2)
asyncio.run(bowl.assign_eaters([person_1, person_2, person_3]))

但是,我的尝试导致以下行为:

(t=0) Person A picks up 2 servings of food, leaving 23 servings in the bowl.
(t=0) Person B picks up 3 servings of food, leaving 20 servings in the bowl.
(t=0) Person C picks up 5 servings of food, leaving 15 servings in the bowl.
(t=4) Person A picks up 2 servings of food, leaving 13 servings in the bowl.
(t=4) Person B picks up 3 servings of food, leaving 10 servings in the bowl.
(t=4) Person C picks up 5 servings of food, leaving 5 servings in the bowl.
(t=8) Person A picks up 2 servings of food, leaving 3 servings in the bowl.
(t=8) Person B picks up 3 servings of food, leaving 0 servings in the bowl.
(t=8) Person C picks up 0 servings of food, leaving 0 servings in the bowl.
The bowl is empty!

可以看出,每个人都在等每个人吃完饭后再伸手去拿碗。查看我的代码,我知道这是因为我等待着asyncio.gather()的eat功能,因此它将在任何人重新开始进食之前等待所有三个人完成进食。 >

我知道这是错误的,但是我不知道我可以在asyncio库中使用什么来解决此问题。我在想eat_from协程会自动重启,只要碗里还有食物。我该如何完成此工作,或者有解决此问题的更好方法?

1 个答案:

答案 0 :(得分:1)

  

我知道[等待所有三个人吃完饭,然后任何人才能再次开始进食]是错误的,但是我不知道我可以在asyncio库中使用什么来解决这个问题。

您可以使用wait(return_when=asyncio.FIRST_COMPLETED)等待任何一个吃完,而不必像当前代码那样等待所有全部。只要食者完成进食,就为该食者生成一个新的协程,有效地“重新启动”它。这需要引用wait返回给食者的任务;这样的引用可以很容易地附加到Task对象上。代码看起来像这样:

async def assign_eaters(self, eaters):
    self.start_time = time.time()
    # create the initial tasks...
    pending = [asyncio.create_task(eater.eat_from(self))
               for eater in eaters]
    # ...and store references to their respective eaters
    for t, eater in zip(pending, eaters):
        t.eater = eater

    while True:
        done, pending = await asyncio.wait(
            pending, return_when=asyncio.FIRST_COMPLETED)
        if self.qty == 0:
            break
        for t in done:
            # re-create the coroutines that have finished
            new = asyncio.create_task(t.eater.eat_from(self))
            new.eater = t.eater
            pending.add(new)
    t = round(time.time() - self.start_time)
    print("The bowl is empty!")

这会产生预期的输出,但会带来一些复杂性。但是,如果您准备好改变自己的方法,则有一个更简单的可能性:让每个食者成为一个独立的演员,继续吃饭,直到碗里没有更多的食物为止。然后,您不必“重新启动”食用器,仅因为它们不会首先退出,至少只要碗中有食物即可。

async def eat_from(self, foodbowl):
    while foodbowl.qty:
        servings_taken = self.serving_size \
            if foodbowl.qty >= self.serving_size else foodbowl.qty
        foodbowl.qty -= servings_taken
        t = round(time.time() - foodbowl.start_time)
        print("(t={}) Person {} picks up {} servings of food, "
              "leaving {} servings in the bowl."
              .format(t, self.name, servings_taken, foodbowl.qty))
        await asyncio.sleep(self.time_to_eat)

assign_eaters不再需要循环并恢复为使用简单的gather

async def assign_eaters(self, eaters):
    self.start_time = time.time()
    await asyncio.gather(*[eater.eat_from(self) for eater in eaters])
    t = round(time.time() - self.start_time)
    print("The bowl is empty!")

此更简单的代码再次产生预期的输出。唯一的“缺点”是更改需要反向控制:碗不再驱动进餐过程,现在由每个食者自动完成,而碗被动地等待他们完成。然而,从问题陈述来看,这似乎不仅可以接受,而且甚至可以追捧。据说,饭碗功能应该使人们“从饭碗开始进食,直到饭碗空了”。 “开始进食”意味着碗只是开始烹饪过程,每个人都自己做饭-第二个版本就是这样。