asyncio.Queue.join зависает: где вызывать task_done
Работник взял элемент из asyncio.Queue и упал на неправильном задании. В очереди уже пусто, но await queue.join() не завершается. Пустота контейнера и окончание работы — разные состояния.
Понять счётчик незавершённых элементов
Каждый put увеличивает счётчик незавершённых заданий. get извлекает объект, но счётчик не уменьшает: работник ещё только начал обработку. task_done сообщает об окончании одной попытки обработки. join ждёт, когда для каждого добавленного элемента придёт такое сообщение.
Поэтому task_done удобно поставить в finally сразу после успешного get. Тогда ожидаемая ошибка обработки тоже завершает учёт элемента. Это не объявляет бизнес-операцию успешной: успех и отказ сохраняются отдельно. Если нужна повторная попытка, она должна явно добавить новое задание или изменить другой учётный механизм.
Не потерять ошибочное задание
В примере очередь содержит число, неверное значение и сигнал остановки. Работник сохраняет результат либо причину отказа, а после каждого извлечения вызывает task_done. Сигнал остановки тоже добавлен через put, следовательно, ему соответствует свой task_done. Именно этот элемент часто забывают посчитать.
Ожидаемые ValueError и TypeError обрабатываются внутри задания. Неожиданная ошибка программы не должна молча превращаться в обычный статус. В рабочем исполнителе владелец задач должен также замечать падение самого работника: один join не заменяет ожидание его Task.
import asyncio
async def main():
queue = asyncio.Queue()
results, rejected = [], []
async def worker():
while True:
item = await queue.get()
try:
if item is None:
return
try:
results.append(int(item) * 2)
except (ValueError, TypeError):
rejected.append(item)
finally:
queue.task_done()
task = asyncio.create_task(worker())
for item in ["3", "bad", "4", None]:
await queue.put(item)
await asyncio.wait_for(queue.join(), timeout=2)
await task
assert results == [6, 8]
assert rejected == ["bad"]
assert queue.empty()
print(results, rejected)
asyncio.run(main())Что проверять при зависании
Проследите каждый путь после get: нормальная обработка, известная ошибка, сигнал остановки. Для каждого должен быть ровно один task_done. Лишний вызов приведёт к ValueError, пропущенный оставит join в ожидании. Не добавляйте task_done в нескольких ветках, если один finally может выразить ту же гарантию.
Отмена работника посреди задания тоже выполнит finally. В таком случае очередь посчитает попытку завершённой, но результат может отсутствовать. Если требуется восстановление после отмены, сохраняйте статус задания отдельно. Счётчик Queue живёт в памяти процесса и не является журналом доставки.
Начать с проверяемого результата
В публичном уроке pytest потренируйте обычный вход и отказ. В своей очереди проверьте их совместный запуск, как в примере. Откройте программу Python, чтобы продолжить с изоляцией состояния и регрессионными тестами.
Продолжить на практике
- Python для продвинутых: ООП, типизация и тестирование ↗
Практический курс-мост от базового Python к разработке поддерживаемых приложений. Вы разберётесь со структурой пакетов, объектной моделью, современной типизацией и автоматическими тестами, а затем соедините эти инструменты в небольшом приложении с хранением данных и логированием.
