Остановка asyncio-программы до и после Python 3.11
В версиях Python 3.11+ (проверено на 3.11.1, 3.12.3) при нажатии CTRL+C и перехвата KeyboardInterrupt
вне цикла, все задачи завершаются и код выхода 0.
from asyncio import sleep, wait, run, create_task
from queue import Queue, Empty
async def worker(q):
while True:
try:
q.get_nowait()
... # обработка
except Empty:
await sleep(0)
async def main():
q = Queue()
tasks = []
for i in range(5):
tasks.append(create_task(worker(q), name=f'worker-{i}'))
await wait(tasks)
try:
run(main())
except KeyboardInterrupt:
pass
Однако на версия ниже (проверено на 3.10.11, 3.9.13, 3.8.10), ошибка KeyboardInterrupt "остаётся" внутри.
Task exception was never retrieved
future: <Task finished name='worker-0' coro=<worker() done, defined at D:\test.py:6> exception=KeyboardInterrupt()>
Traceback (most recent call last):
File "D:\test.py", line 24, in <module>
run(main())
File "D:\Programs\Python\Python310\lib\asyncio\runners.py", line 44, in run
return loop.run_until_complete(main)
File "D:\Programs\Python\Python310\lib\asyncio\base_events.py", line 636, in run_until_complete
self.run_forever()
File "D:\Programs\Python\Python310\lib\asyncio\windows_events.py", line 321, in run_forever
super().run_forever()
File "D:\Programs\Python\Python310\lib\asyncio\base_events.py", line 603, in run_forever
self._run_once()
File "D:\Programs\Python\Python310\lib\asyncio\base_events.py", line 1909, in _run_once
handle._run()
File "D:\Programs\Python\Python310\lib\asyncio\events.py", line 80, in _run
self._context.run(self._callback, *self._args)
File "D:\test.py", line 9, in worker
q.get_nowait()
File "D:\Programs\Python\Python310\lib\queue.py", line 199, in get_nowait
return self.get(block=False)
File "D:\Programs\Python\Python310\lib\queue.py", line 154, in get
def get(self, block=True, timeout=None):
KeyboardInterrupt
Есть временное решение, которое работает, но в документации/списке изменений я не нашёл такого изменения.
...
async def as_worker(coro, tasks):
try:
await coro
except KeyboardInterrupt as e:
for task in tasks:
task.cancel()
...
async def main():
q = Queue()
tasks = []
for i in range(5):
tasks.append(create_task(as_worker(worker(q), tasks), name=f'worker-{i}'))
print('wait')
await wait(tasks)
...
Ответы (1 шт):
Возможность прерывать asyncio-программы через обработку Ctrl-C действительно была добавлена с версии 3.11 - это описано в документации к asyncio/runners.py:
Handling Keyboard Interruption - New in version 3.11
Нажатие Ctrl-C во время работы python-программы отправляет процессу сигнал signal.SIGINT
, что вызывает исключение KeyboardInterrupt в основном потоке.
Однако это не работает нормально с asyncio, поскольку такой выброс исключения может прервать внутренние процессы asyncio и может привести к зависанию программы при выходе из нее.
Чтобы устранить эту проблему, с версии 3.11 для asyncio signal.SIGINT обрабатывается следующим образом:
asyncio.Runner.run() устанавливает собственный обработчик signal.SIGINT перед выполнением любого пользовательского кода и удаляет его при выходе из функции.
Runner создает основную задачу для выполнения переданной корутины.
Когда передается Ctrl-C, то ранее созданный обработчик сигналов отменяет основную задачу путем вызова
asyncio.Task.cancel()
, который вызывает исключение asyncio.CancelledError внутри основной задачи. Это приводит к тому, что стек Python раскручивается, блоки try/except и try/finally могут использоваться для очистки ресурсов. Уже после отмены основной задачи asyncio.Runner.run() вызывает KeyboardInterrupt.Пользователь может написать цикл, который не может быть прерван с помощью asyncio.Task.cancel(). В этом случае второе последующее нажатие Ctrl-C немедленно вызовет KeyboardInterrupt, не отменяя основную задачу.
До 3.11 для нормального завершения asyncio дописывали свои обработчики signal.SIGINT - один из примеров.