Остановка 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 шт):

Автор решения: mrgervant

Возможность прерывать 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 обрабатывается следующим образом:

  1. asyncio.Runner.run() устанавливает собственный обработчик signal.SIGINT перед выполнением любого пользовательского кода и удаляет его при выходе из функции.

  2. Runner создает основную задачу для выполнения переданной корутины.

  3. Когда передается Ctrl-C, то ранее созданный обработчик сигналов отменяет основную задачу путем вызова asyncio.Task.cancel(), который вызывает исключение asyncio.CancelledError внутри основной задачи. Это приводит к тому, что стек Python раскручивается, блоки try/except и try/finally могут использоваться для очистки ресурсов. Уже после отмены основной задачи asyncio.Runner.run() вызывает KeyboardInterrupt.

  4. Пользователь может написать цикл, который не может быть прерван с помощью asyncio.Task.cancel(). В этом случае второе последующее нажатие Ctrl-C немедленно вызовет KeyboardInterrupt, не отменяя основную задачу.

До 3.11 для нормального завершения asyncio дописывали свои обработчики signal.SIGINT - один из примеров.

→ Ссылка