В документации по Целевым группам говорится:
Два базовых исключения обрабатываются особым образом: если какая-либо задача завершается с ошибкой
KeyboardInterruptилиSystemExit, группа задач по-прежнему отменяет оставшиеся задачи и ждет их, но затем вместоKeyboardInterruptилиSystemExitповторно повышается первоначальнаяExceptionGroupилиBaseExceptionGroup.
Это заставляет меня поверить, учитывая следующий код:
import asyncio
async def task():
await asyncio.sleep(10)
async def run() -> None:
try:
async with asyncio.TaskGroup() as tg:
t1 = tg.create_task(task())
t2 = tg.create_task(task())
print("Done")
except KeyboardInterrupt:
print("Stopped")
asyncio.run(run())
бег и нажатие Ctrl-C должно привести к печати Stopped; но на самом деле исключение не перехватывается:
^CTraceback (most recent call last):
File "<python>/asyncio/runners.py", line 118, in run
return self._loop.run_until_complete(task)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "<python>/asyncio/base_events.py", line 685, in run_until_complete
return future.result()
^^^^^^^^^^^^^^^
File "<module>/__init__.py", line 8, in run
async with asyncio.TaskGroup() as tg:
File "<python>/asyncio/taskgroups.py", line 134, in __aexit__
raise propagate_cancellation_error
File "<python>/asyncio/taskgroups.py", line 110, in __aexit__
await self._on_completed_fut
asyncio.exceptions.CancelledError
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "<frozen runpy>", line 189, in _run_module_as_main
File "<frozen runpy>", line 148, in _get_module_details
File "<frozen runpy>", line 112, in _get_module_details
File "<module>/__init__.py", line 15, in <module>
asyncio.run(run())
File "<python>/asyncio/runners.py", line 194, in run
return runner.run(main)
^^^^^^^^^^^^^^^^
File "<python>/asyncio/runners.py", line 123, in run
raise KeyboardInterrupt()
KeyboardInterrupt
Что мне не хватает? Каков правильный способ обнаружения KeyboardInterrupt?






Это на самом деле не вина TaskGroup. Попробуйте запустить это:
>>> async def other_task():
... try:
... await asyncio.sleep(10)
... except KeyboardInterrupt:
... print("Stopped")
>>>
>>> asyncio.run(other_task())
KeyboardInterrupt
>>>
Это тоже не печатается. Ни это:
>>> async def other_task():
... try:
... await asyncio.sleep(10)
... except Exception as err:
... print("Stopped by", err)
>>>
>>> asyncio.run(other_task())
KeyboardInterrupt
>>>
Здесь нельзя поймать KeyboardInterrupt.
Я не могу сказать, что знаю точно, почему, но могу сказать, что именно поэтому asyncio.shield бесполезен для защиты задач от отмены, потому что он требует написания собственных обработчиков сигналов для asyncio, потому что отмена задачи вызывает внутренний триггер KeyboardInterrupt.
trio - создан Натаниэлем Дж. Смитом , который первым придумал современный структурированный параллелизм - четко делает то, что вы задумали.
# NOTE: somehow ctrl+c doesn't work in ptpython. Following ran on default python shell
# Python 3.12.1 (tags/v3.12.1:2305ca5, Dec 7 2023, 22:03:25) [MSC v.1937 64 bit (AMD64)] on win32
>>> import trio
>>> async def task():
... await trio.sleep(5)
...
>>> async def run():
... try:
... async with trio.open_nursery() as nursery:
... for _ in range(10):
... nursery.start_soon(task)
... except* KeyboardInterrupt:
... print("Nursery caught KeyboardInterrupt!")
...
>>> trio.run(run)
Nursery caught KeyboardInterrupt!
>>>
# Python 3.11.2 (main, Mar 13 2023, 12:18:29) [GCC 12.2.0] on linux
>>> import trio
>>> from exceptiongroup import catch
>>> async def task():
... await trio.sleep(5)
...
>>> async def run():
... def handle_keyboard_interrupt(excgroup):
... nonlocal nursery
... print("Nursery caught KeyboardInterrupt!")
... nursery.cancel_scope.cancel()
...
... with catch({KeyboardInterrupt: handle_keyboard_interrupt}):
... async with trio.open_nursery() as nursery:
... for _ in range(10):
... nursery.start_soon(task)
...
>>> trio.run(run)
^CNursery caught KeyboardInterrupt!
>>>
который не является супом обратных вызовов в отличие от asyncio, что делает его гораздо более стабильным, интуитивно понятным и прогнозирующим. (Я не говорю, что asyncio — это мусор; это был правильный вариант, когда он был создан.)
Так что обязательно проверьте trio, когда то, что вы пытаетесь сделать, требует этого. asyncio.TaskGroup — это просто попытка asyncio структурированного параллелизма, достигнутая trio.Nursery, и оригинал делает свою работу лучше, чем asyncio, который неизбежно страдает от того, что внутренние компоненты представляют собой суп обратных вызовов.
Обновлено: также вы можете использовать trio-asyncio или подобные оболочки для запуска асинхронного цикла в трио или смешивать библиотеки на основе потоков с трио
Вот подсказка, почему это происходит:
>>> async def sync_task():
... try:
... time.sleep(10)
... except KeyboardInterrupt:
... print("Stopping")
>>> asyncio.run(sync_task())
Stopping
KeyboardInterrupt
Если мы намеренно заблокируем поток вызовом time.sleep(), он перехватит KeyboardInterrupt. (Несмотря на asyncio, также снова поднимите ставку)
Это то же самое, что и это.
>>> def sync_task():
... try:
... time.sleep(10)
... except KeyboardInterrupt:
... print("Stopping")
>>> sync_task()
Stopping
Теперь мы установили, что можем перехватывать KeyboardInterrupt в разделе синхронного кода независимо от того, является ли окружение асинхронным контекстом или нет.
Тогда мы можем снова обратить внимание на:
Синхронен ли
await asyncio.sleep(10)?
И это может показаться очевидным, но это не так!
...Но тогда что же выполняется, если оно не синхронно?
Это основная причина, по которой asyncio ничего не может зацепить KeyboardInterrupt. Потому что это не наша задача, а основной поток, который последовательно проверяет, должно ли сработать то или иное событие или нет.
Другими словами, мы просто отключили основной цикл на KeyboardInterrupt, пока наши задачи приостановлены, ожидая, пока цикл основного потока вызовет обратный вызов для события «Позвони мне ПОСЛЕ того, как пройдет 10 секунд». Следовательно, у нашей бедной задачи не было шансов когда-либо перехватить это исключение.
Это связано с тем, что в отличие от других исключений, которые возвращаются как результат задачи (а затем, вероятно, повторно выбрасываются по ключевому слову await), KeyboardInterrupt является срочным исключением, сигнализирующим о необходимости остановить все, что мы делаем, поэтому цикл событий решает остановиться и начать очистку, если это возможно. .
Цитата из Обработка Control-C в Python и Trio Натаниэля Дж. Смита:
По умолчанию интерпретатор Python настраивает все так, чтобы Control-C вызывал материализацию исключения KeyboardInterrupt в какой-то момент вашего кода.
Это очень приятно! Если ваш код случайно попал в бесконечный цикл, он вырвется из него. Если у вас есть код очистки в блокахfinally, он запускается. Он показывает обратную трассировку, чтобы вы могли найти этот бесконечный цикл.
В этом преимущество подхода KeyboardInterrupt: даже если вы вообще не думали о Control-C во время написания программы, он все равно делает что-то чертовски разумное — скажем, в 99% случаев.
lock.acquire() try: do_stuff() # <- do_something_else() # <- control-C anywhere here is safe and_some_more() # <- finally: lock.release()Но что, если нам не повезет?
lock.acquire() # <- control-C could happen here try: ... finally: # <- or here lock.release()Если KeyboardInterrupt происходит в одной из двух точек, отмеченных выше, то это отстой: исключение будет распространяться, но наша блокировка никогда не будет снята.
... KeyboardInterrupt — такое мощное и опасное сообщение для Python — оно не просто Exception, но почти как SIGINT для Python. (При уничтожении задачи Windows активируется KeyboardInterrupt, поскольку у него нет SIGINT)
Это главный недостаток параллелизма на основе обратного вызова, который мы годами использовали в Javascript и многих других, включая future и asyncio в Python.
Из истории Python 1991~2015 годов:
Выпуск Python 1.0 (Python 1, 1991 г.)
Библиотека Asyncore (Python 1, 1996 г.)
Добавлена поддержка генераторов (Python 2.2, 2001 г.).
Витой (Python 2.X, 2002 г.)
Добавлена поддержка Generator.send() (Python 2.5, 2005 г.)
Будущая библиотека (Python 3.2, 2011 г.)
библиотека asyncio (Python 3.4, 2014 г.)
добавлен async/await (Python 3.5, 2015)
asyncio никогда не разрабатывался с учетом async/await, но основан на обратных вызовах, как это делает недавний Javascript. (Первоначальная версия javascript в 1995 году не имела параллелизма iirc)
Итак, то, как мы используем asyncio с async / await, в наши дни под капотом находится полный гибридный беспорядок, пытающийся заставить суп обратных вызовов работать над чем-то принципиально другим.
Когда мы вызываем asyncio.run(async_func) — он выполняется в следующем порядке:
# NOTE: all comment with ^^^^^^ prefix are my comments explaining relevant parts
# -----------------------------------------------------------------------
# asyncio/runners.py
def run(main, *, debug=None, loop_factory=None):
...
with Runner(debug=debug, loop_factory=loop_factory) as runner:
return runner.run(main)
# ^^^^^ entry point
# -----------------------------------------------------------------------
# asyncio/runners.py
class Runner:
...
def run(self, coro, *, context=None):
"""Run a coroutine inside the embedded event loop."""
...
task = self._loop.create_task(coro, context=context)
# ^^^^^^ All async funcs are wrapped in task and is immediately pending for execution
if (threading.current_thread() is threading.main_thread()
and signal.getsignal(signal.SIGINT) is signal.default_int_handler
):
# ^^^^^^ First hint of asyncio's internals being thread based
sigint_handler = functools.partial(self._on_sigint, main_task=task)
else:
sigint_handler = None
self._interrupt_count = 0
try:
return self._loop.run_until_complete(task)
# ^^^^^ entry point
except exceptions.CancelledError:
if self._interrupt_count > 0:
uncancel = getattr(task, "uncancel", None)
if uncancel is not None and uncancel() == 0:
raise KeyboardInterrupt()
# ^^^^^^ This is why KeyboardInterrupt raises from task cancelation
raise # CancelledError
finally:
...
# -----------------------------------------------------------------------
# asyncio/base_events.py
# This file and class is abstract; Actual Loops depends on OS
# due to difference event handling method per OS.
class BaseEventLoop(events.AbstractEventLoop):
def run_until_complete(self, future):
...
new_task = not futures.isfuture(future)
future = tasks.ensure_future(future, loop=self)
...
future.add_done_callback(_run_until_complete_cb)
# ^^^^^^ Another hint of callback soup
try:
self.run_forever()
# ^^^^^ entry point
...
...
def run_forever(self):
"""Run until stop() is called."""
...
try:
self._thread_id = threading.get_ident()
...
while True:
self._run_once()
# ^^^^^^ entry point (complex callback & event checks, skipping)
if self._stopping:
break
...
Довольно сложный, он вращается вокруг обратного вызова, и мы не можем найти прямую точку, где задача действительно получает executed - даже в self._run_once() это просто Runner класс, запускающий прослушиватели событий в очереди.
Неудивительно, что здесь так много ошибок и проблем со стабильностью, что мы поражаемся тому, сколько усилий нужно было вложить в django, flask, fastAPI и т. д., чтобы заставить его работать с высокой стабильностью.
Цитируем Несколько мыслей об асинхронном дизайне API в пост-асинхронном/ожидающем мире снова от NJS:
Обзор и подведение итогов: что вообще такое «async/await-native»?
В предыдущих асинхронных API для Python использование программирования, ориентированного на обратные вызовы, привело к изобретению целого набора соглашений, которые фактически составляют целый специальный язык программирования...
Результат в некоторой степени аналогичен плохим старым временам до структурного программирования, когда базовые конструкции, такие как вызовы функций и циклы, приходилось конструировать на лету с помощью примитивных инструментов, таких как goto.
На практике написать правильный код в таком стиле чрезвычайно сложно, особенно когда начинаешь думать о граничных условиях.
Теперь, когда в Python есть async/await, можно начать использовать собственные механизмы Python для решения этих проблем.
Такое ограничение asyncio и то, насколько эффективным было curio, казалось, привело к тому, что NJS завершила разработку концепции структурированного параллелизма и создала trio, открывая для Python путь к светлому будущему без будущего!
(Фактическое изображение прикреплено в *Некоторые мысли об асинхронном дизайне API в мире пост-асинхронности/ожидания)
Не могу сказать, что я эксперт, но результаты моего тестирования: Простой сервер удаленного выполнения Python, подключенный к Discord (да, я действительно пожертвовал здесь своим Raspberry Pi) практически один и тот же код в asyncio умирал каждую неделю , а trio rewrite ни разу не умер за 3 месяца.
asyncio мусорНет, я думаю, что это была лучшая библиотека, которая у нас была, когда она вышла, и до сих пор, пока мы не получили trio. Я считаю, что трио было возможно, потому что было асинхронно!
Цитируем Несколько мыслей об асинхронном дизайне API в пост-асинхронном/ожидающем мире снова и снова от NJS:
Следует ли «исправить» asyncio, чтобы иметь API-интерфейс async/await-native в стиле Curio?
Я не понимаю, как это можно сделать, не выбросив и не переписав большую часть asyncio. ... но части цепочки обратных вызовов довольно глубоко встроены в asyncio в том виде, в котором он существует в настоящее время.
Похоже, сейчас уже слишком поздно переписывать его, учитывая, что он находится в стандарте уже почти десятилетие - но я бы хотел, чтобы asyncio прекратил попытки имитировать структурированный параллелизм, как недавнее добавление TaskGroup, и лучше переписал или отказался от поддержки в пользу других библиотек - но поскольку это Я полагаю, что это невозможно, они добавляют это, по крайней мере, для улучшения текущей ситуации.
В любом случае, он отлично подходит для многих простых случаев использования!
Если вы не можете его поймать, какова цель ре-рейза KeyboardInterrupt?
@Амадан, извини, скоро скоро, компания распадается!
@Амадан, извини, что потребовалось некоторое время, кстати, могу ли я спросить тебя, насколько ты знаком с asyncio и его внутренним устройством? ответ может быть намного длиннее в зависимости от вашего понимания внутреннего устройства asyncio, о котором, я смутно думаю, вы уже знаете.
Считайте меня нубом (мне гораздо удобнее решать задачи JavaScript, чем Python). Кстати, я бегло просмотрел trio и прочитал «Оператор Go считается вредным», и я думаю, что попытаюсь принять его, но: 1) когда я запускаю ваш код, исключение также не перехватывается (самым крайним исключением является exceptiongroup.BaseExceptionGroup, а не KeyboardInterrupt) и 2) мне все равно было бы любопытно, почему мой код не работает должным образом (или, если уж на то пошло, почему я не могу заставить работать и ваш код). Если у вас есть время, конечно. (Протестировано на Python 3.10.14, трио 0.25.0.)
@Амадан, здорово! Да, и exceptiongroup.BaseExceptionGroup это может быть мой trio устаревший или какое-то гоночное состояние. В отличие от asyncio (извините их разработчикам, что продолжают это бить), трио обрабатывает множество ошибок базовых задач. Это означает, что если возникает несколько дочерних задач, все броски хорошо упаковываются внутри trio.MultiError iirc. Возможно, я остановился слишком рано и получил только один бросок, обновлю и эту часть, дайте мне 1 час на подготовку!
Думаю, я нашел его, проведя еще некоторое время с документами . Теперь по умолчанию даже отдельные исключения создают группы исключений (а возможность не делать этого, судя по всему, устарела). Я не думаю, что все библиотеки, которые мне понадобятся, смогут работать с 3.11, так что нет except* для меня... так что, думаю, мне нужно научиться использовать exceptiongroup библиотеку.
@Amadan Я не позволю тебе отметить «принято» и уйти без полного контекста ситуации! Это заняло намного больше времени, чем ожидалось, но наконец-то обновилось в полной мере!
Ух ты, это так много! Мне понадобится некоторое время, чтобы переварить. Но, по крайней мере, теперь я знаю, как это написать в trio. И вы правы, это намного проще. Я могу только извиниться за то, что ваш тяжелый труд останется без вознаграждения, я уже потратил свой один голос :D
@Amadan Когда я увидел, что вы уже искали и продолжили читать заявление Go, признанное вредным, которое я еще даже не связал - я сразу понял, что у вас есть страсть, и ни одно мое время не остается без вознаграждения для таких людей! Наслаждайтесь структурированным параллелизмом, товарищ питонист! (кстати, отдельная дочерняя задача в детской также может перехватывать KeyboardInterrupt естественным и предсказуемым образом без ExceptionGroup)
О, кстати, возможно, будет полезно это прочитать ТАК ответьте - я не знал
asyncio.TaskGroup, было ли это хрупким, думаю, оно действительно не очень хорошо справляется сanyioилиtrio