#asynchronous #async-await #python-asyncio #python-3.7
#асинхронный #асинхронный -ожидание #python-asyncio #python-3.7
Вопрос:
Я занимаюсь асинхронным программированием на Python со следующей проблемой:
Имитируйте, что несколько человек едят из одной миски с заданным количеством порций пищи. Каждый пользователь может съесть x порций пищи за раз, а затем пережевывать пищу в течение y секунд (имитируется с помощью блокирующего вызова). Человек может принимать и пережевывать пищу независимо от других людей, пока в миске еще есть еда.
Определите классы для каждого едока и миски с едой. Конечная цель — иметь функцию в классе food bowl, которая принимает список пользователей и заставляет их начинать есть из миски, пока миска не опустеет. Сообщение должно печататься в стандартный вывод всякий раз, когда пользователь берет еду из миски.
Например, если у меня есть миска для еды с 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:
Я знаю, что [ждать, пока все три человека закончат есть, прежде чем кто-либо сможет снова начать есть] неправильно, но я не знаю, что я могу использовать в библиотеке 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!")
Этот более простой код снова приводит к ожидаемому результату. Единственным «недостатком» является то, что изменение потребовало инвертирования управления: миска больше не управляет процессом приема пищи, теперь это делается автономно каждым едоком, а миска пассивно ждет, пока они закончат. Однако, глядя на формулировку проблемы, это кажется не только приемлемым, но, возможно, даже искомым решением. Указано, что функция food bowl должна заставить людей «начинать есть из миски, пока миска не опустеет». «Начать есть» подразумевает, что чаша просто инициирует процесс, и что каждый человек ест сам — именно так работает вторая версия.