重新启动Async Coroutine,无需等待其他协同程序完成

问题描述 投票:1回答:1

我正在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(),因此它将等待所有三个人吃完之后再开始进食。

我知道这是错的,但我不知道我可以在asyncio库中使用什么来解决这个问题。只要碗里还有食物,我想的是eat_from coroutine会自动重启。我如何实现这一目标,还是有更好的方法解决这个问题?

asynchronous async-await python-asyncio python-3.7
1个回答
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!")

这个更简单的代码再次产生预期的输出。唯一的“缺点”是变化需要反转控制:碗不再驱动进食过程,现在由每个食客自主完成,碗被动地等待它们完成。然而,从问题陈述来看,这似乎不仅可以接受,而且甚至可能是广受欢迎的解决方案。据说食物碗的功能应该让人们“从碗里开始吃东西直到碗是空的”。 “开始吃”意味着碗只是启动过程,每个人都自己进食 - 这就是第二个版本的工作方式。

© www.soinside.com 2019 - 2024. All rights reserved.