> **Источник:** https://python-all.ru/3.14/library/asyncio-dev.html
>
> «Документация Python на русском» – неофициальный перевод официальной документации Python: версии от 2.6 до 3.16, полнотекстовый поиск, английский оригинал рядом с переводом. Эта Markdown-версия страницы предназначена для работы с LLM: вставьте её в ChatGPT, Claude или Cursor.

---

# Разработка с asyncio

Асинхронное программирование отличается от классического «последовательного» программирования.

На этой странице перечислены типичные ошибки и ловушки, а также объясняется, как их избежать.

## Режим отладки

По умолчанию asyncio работает в производственном режиме. Для упрощения разработки в asyncio есть *режим отладки*.

Включить режим отладки asyncio можно несколькими способами:

- Установка переменной окружения [`PYTHONASYNCIODEBUG`](https://python-all.ru/3.14/using/cmdline.html#envvar-PYTHONASYNCIODEBUG) в значение `1`.
- Использование [режима разработки Python](https://python-all.ru/3.14/library/devmode.html#devmode).
- Передача `debug=True` в [`asyncio.run()`](https://python-all.ru/3.14/library/asyncio-runner.html#asyncio.run).
- Вызов [`loop.set_debug()`](https://python-all.ru/3.14/library/asyncio-eventloop.html#asyncio.loop.set_debug).

В дополнение к включению режима отладки также рекомендуется:

- Установка уровня журналирования [логгера asyncio](https://python-all.ru/3.14/library/asyncio-dev.html#asyncio-logger) на [`logging.DEBUG`](https://python-all.ru/3.14/library/logging.html#logging.DEBUG); например, следующий фрагмент кода можно выполнить при запуске приложения:

  ```python
  logging.basicConfig(level=logging.DEBUG)
  ```
- Настройка модуля [`warnings`](https://python-all.ru/3.14/library/warnings.html#module-warnings) на отображение предупреждений [`ResourceWarning`](https://python-all.ru/3.14/library/exceptions.html#ResourceWarning). Один из способов сделать это – использовать параметр командной строки [`-W`](https://python-all.ru/3.14/using/cmdline.html#cmdoption-W) `default`.

Когда режим отладки включён:

- Многие небезопасные по отношению к потокам API asyncio (например, методы [`loop.call_soon()`](https://python-all.ru/3.14/library/asyncio-eventloop.html#asyncio.loop.call_soon) и [`loop.call_at()`](https://python-all.ru/3.14/library/asyncio-eventloop.html#asyncio.loop.call_at)) вызывают исключение, если их вызывают не из того потока.
- Время выполнения селектора ввода-вывода журналируется, если выполнение операции ввода-вывода занимает слишком много времени.
- Колбэки, выполняющиеся дольше 100 миллисекунд, журналируются. Атрибут [`loop.slow_callback_duration`](https://python-all.ru/3.14/library/asyncio-eventloop.html#asyncio.loop.slow_callback_duration) можно использовать для задания минимальной продолжительности выполнения в секундах, которая считается «медленной».

## Параллелизм и многопоточность

Цикл событий работает в потоке (обычно в главном) и выполняет все колбэки и задачи в своём потоке. Пока задача выполняется в цикле событий, никакие другие задачи не могут выполняться в том же потоке. Когда задача выполняет выражение `await`, она приостанавливается, и цикл событий переходит к следующей задаче.

Для планирования [колбэка](https://python-all.ru/3.14/glossary.html#term-callback) из другого потока ОС следует использовать метод [`loop.call_soon_threadsafe()`](https://python-all.ru/3.14/library/asyncio-eventloop.html#asyncio.loop.call_soon_threadsafe). Пример:

```python
loop.call_soon_threadsafe(callback, *args)
```

Почти все объекты asyncio не являются потокобезопасными, что обычно не является проблемой, если только нет кода, который работает с ними вне задачи или колбэка. Если такому коду нужно вызвать низкоуровневый API asyncio, следует использовать метод [`loop.call_soon_threadsafe()`](https://python-all.ru/3.14/library/asyncio-eventloop.html#asyncio.loop.call_soon_threadsafe), например:

```python
loop.call_soon_threadsafe(fut.cancel)
```

Для планирования объекта корутины из другого потока ОС следует использовать функцию [`run_coroutine_threadsafe()`](https://python-all.ru/3.14/library/asyncio-task.html#asyncio.run_coroutine_threadsafe). Она возвращает [`concurrent.futures.Future`](https://python-all.ru/3.14/library/concurrent.futures.html#concurrent.futures.Future) для доступа к результату:

```python
async def coro_func():
     return await asyncio.sleep(1, 42)

# Позже в другом потоке ОС:

future = asyncio.run_coroutine_threadsafe(coro_func(), loop)
# Ожидать результат:
result = future.result()
```

Для обработки сигналов цикл событий должен выполняться в главном потоке.

Метод [`loop.run_in_executor()`](https://python-all.ru/3.14/library/asyncio-eventloop.html#asyncio.loop.run_in_executor) можно использовать с [`concurrent.futures.ThreadPoolExecutor`](https://python-all.ru/3.14/library/concurrent.futures.html#concurrent.futures.ThreadPoolExecutor) или [`InterpreterPoolExecutor`](https://python-all.ru/3.14/library/concurrent.futures.html#concurrent.futures.InterpreterPoolExecutor) для выполнения блокирующего кода в другом потоке ОС, не блокируя поток ОС, в котором работает цикл событий.

В настоящее время нет возможности планировать корутины или колбэки напрямую из другого процесса (например, запущенного с помощью [`multiprocessing`](https://python-all.ru/3.14/library/multiprocessing.html#module-multiprocessing)). В разделе [Методы цикла событий](https://python-all.ru/3.14/library/asyncio-eventloop.html#asyncio-event-loop-methods) перечислены API, которые могут читать из каналов и отслеживать файловые дескрипторы, не блокируя цикл событий. Кроме того, API asyncio для [подпроцессов](https://python-all.ru/3.14/library/asyncio-subprocess.html#asyncio-subprocess) позволяют запустить процесс и взаимодействовать с ним из цикла событий. Наконец, упомянутый выше метод [`loop.run_in_executor()`](https://python-all.ru/3.14/library/asyncio-eventloop.html#asyncio.loop.run_in_executor) также можно использовать с [`concurrent.futures.ProcessPoolExecutor`](https://python-all.ru/3.14/library/concurrent.futures.html#concurrent.futures.ProcessPoolExecutor) для выполнения кода в другом процессе.

## Выполнение блокирующего кода

Блокирующий (связанный с процессором) код не следует вызывать напрямую. Например, если функция выполняет интенсивные вычисления в течение 1 секунды, все параллельные задачи asyncio и операции ввода-вывода будут задержаны на 1 секунду.

Исполнитель можно использовать для выполнения задачи в другом потоке, в том числе в другом интерпретаторе, или даже в другом процессе, чтобы не блокировать поток ОС с циклом событий. Подробнее см. метод [`loop.run_in_executor()`](https://python-all.ru/3.14/library/asyncio-eventloop.html#asyncio.loop.run_in_executor).

## Журналирование

asyncio использует модуль [`logging`](https://python-all.ru/3.14/library/logging.html#module-logging), а все логирование выполняется через логгер `"asyncio"`.

Уровень логирования по умолчанию – [`logging.INFO`](https://python-all.ru/3.14/library/logging.html#logging.INFO). Его можно легко изменить:

```python
logging.getLogger("asyncio").setLevel(logging.WARNING)
```

Сетевое логирование может блокировать цикл событий. Рекомендуется использовать отдельный поток для обработки логов или неблокирующий ввод-вывод. Пример см. в [разделе «Обработчики, которые блокируют выполнение»](https://python-all.ru/3.14/howto/logging-cookbook.html#blocking-handlers).

## Обнаружение никогда не ожидаемых корутин

Когда корутинная функция вызывается, но не ожидается (например, `coro()` вместо `await coro()`) или корутина не запланирована с помощью [`asyncio.create_task()`](https://python-all.ru/3.14/library/asyncio-task.html#asyncio.create_task), asyncio выдаёт сообщение [`RuntimeWarning`](https://python-all.ru/3.14/library/exceptions.html#RuntimeWarning):

```python
import asyncio

async def test():
    print("never scheduled")

async def main():
    test()

asyncio.run(main())
```

Вывод:

```python
test.py:7: RuntimeWarning: coroutine 'test' was never awaited
  test()
```

Вывод в режиме отладки:

```python
test.py:7: RuntimeWarning: coroutine 'test' was never awaited
Coroutine created at (most recent call last)
  File "../t.py", line 9, in <module>
    asyncio.run(main(), debug=True)

  < .. >

  File "../t.py", line 7, in main
    test()
  test()
```

Обычно это исправляется либо ожиданием корутины, либо вызовом функции [`asyncio.create_task()`](https://python-all.ru/3.14/library/asyncio-task.html#asyncio.create_task):

```python
async def main():
    await test()
```

## Обнаружение никогда не извлечённых исключений

Если вызван метод [`Future.set_exception()`](https://python-all.ru/3.14/library/asyncio-future.html#asyncio.Future.set_exception), но сам объект Future никогда не ожидается, исключение не будет передано пользовательскому коду. В этом случае asyncio выдаёт сообщение в журнал при сборке мусора для этого Future.

Пример необработанного исключения:

```python
import asyncio

async def bug():
    raise Exception("not consumed")

async def main():
    asyncio.create_task(bug())

asyncio.run(main())
```

Вывод:

```python
Task exception was never retrieved
future: <Task finished coro=<bug() done, defined at test.py:3>
  exception=Exception('not consumed')>

Traceback (most recent call last):
  File "test.py", line 4, in bug
    raise Exception("not consumed")
Exception: not consumed
```

[Включите режим отладки](https://python-all.ru/3.14/library/asyncio-dev.html#asyncio-debug-mode), чтобы получить трассировку стека на момент создания задачи:

```python
asyncio.run(main(), debug=True)
```

Вывод в режиме отладки:

```python
Task exception was never retrieved
future: <Task finished coro=<bug() done, defined at test.py:3>
    exception=Exception('not consumed') created at asyncio/tasks.py:321>

source_traceback: Object created at (most recent call last):
  File "../t.py", line 9, in <module>
    asyncio.run(main(), debug=True)

< .. >

Traceback (most recent call last):
  File "../t.py", line 4, in bug
    raise Exception("not consumed")
Exception: not consumed
```

## Рекомендации по работе с асинхронными генераторами

Чтобы писать правильный и эффективный код на asyncio, нужно знать о некоторых подводных камнях. В этом разделе собраны основные рекомендации, которые могут сэкономить часы отладки.

### Явное закрытие асинхронных генераторов

Рекомендуется вручную закрывать [асинхронный генератор](https://python-all.ru/3.14/glossary.html#term-asynchronous-generator-iterator). Если генератор завершается досрочно – например, из-за исключения, возникшего в теле цикла `async for`, – его асинхронный код очистки может выполниться в неожиданном контексте. Это может произойти уже после завершения задач, от которых он зависит, или при остановке цикла событий, когда срабатывает сборщик мусора для async-генератора.

Чтобы этого избежать, явно закрывайте генератор, вызывая его метод [`aclose()`](https://python-all.ru/3.14/reference/expressions.html#agen.aclose), или используйте контекстный менеджер [`contextlib.aclosing()`](https://python-all.ru/3.14/library/contextlib.html#contextlib.aclosing):

```python
import asyncio
import contextlib

async def gen():
  yield 1
  yield 2

async def func():
  async with contextlib.aclosing(gen()) as g:
    async for x in g:
      break  # Не выполнять итерацию до конца

asyncio.run(func())
```

Как уже говорилось, код очистки таких асинхронных генераторов откладывается. Следующий пример показывает, что финализация асинхронного генератора может произойти в неожиданном порядке:

```python
import asyncio
work_done = False

async def cursor():
    try:
        yield 1
    finally:
        assert work_done

async def rows():
    global work_done
    try:
        yield 2
    finally:
        await asyncio.sleep(0.1) # имитировать некоторую асинхронную работу
        work_done = True

async def main():
    async for c in cursor():
        async for r in rows():
            break
        break

asyncio.run(main())
```

Для этого примера получаем следующий вывод:

```python
unhandled exception during asyncio.run() shutdown
task: <Task finished name='Task-3' coro=<<async_generator_athrow without __name__>()> exception=AssertionError()>
Traceback (most recent call last):
  File "example.py", line 6, in cursor
    yield 1
asyncio.exceptions.CancelledError

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "example.py", line 8, in cursor
    assert work_done
           ^^^^^^^^^
AssertionError
```

Асинхронный генератор `cursor()` был финализирован до генератора `rows` – неожиданное поведение.

Пример можно исправить, явно закрыв async-генераторы `cursor` и `rows`:

```python
async def main():
    async with contextlib.aclosing(cursor()) as cursor_gen:
        async for c in cursor_gen:
            async with contextlib.aclosing(rows()) as rows_gen:
                async for r in rows_gen:
                    break
            break
```

### Создавайте асинхронные генераторы только при работающем цикле событий

Рекомендуется создавать [асинхронные генераторы](https://python-all.ru/3.14/glossary.html#term-asynchronous-generator-iterator) только после того, как цикл событий уже создан.

Чтобы гарантировать надёжное закрытие асинхронных генераторов, цикл событий использует функцию [`sys.set_asyncgen_hooks()`](https://python-all.ru/3.14/library/sys.html#sys.set_asyncgen_hooks) для регистрации колбэков. Эти колбэки обновляют список запущенных асинхронных генераторов, поддерживая его в согласованном состоянии.

При вызове функции [`loop.shutdown_asyncgens()`](https://python-all.ru/3.14/library/asyncio-eventloop.html#asyncio.loop.shutdown_asyncgens) запущенные генераторы корректно останавливаются, а список очищается.

Асинхронный генератор вызывает соответствующий системный хук при первой итерации. Одновременно генератор запоминает, что хук уже был вызван, и больше не вызывает его.

Поэтому, если итерация начинается до создания цикла событий, цикл событий не сможет добавить генератор в свой список активных генераторов – хуки устанавливаются уже после того, как генератор попытался их вызвать. В результате цикл событий не сможет при необходимости завершить такой генератор.

Рассмотрим следующий пример:

```python
import asyncio

async def agenfn():
    try:
        yield 10
    finally:
        await asyncio.sleep(0)

with asyncio.Runner() as runner:
    agen = agenfn()
    print(runner.run(anext(agen)))
    del agen
```

Вывод:

```python
10
Exception ignored while closing generator <async_generator object agenfn at 0x000002F71CD10D70>:
Traceback (most recent call last):
  File "example.py", line 13, in <module>
    del agen
        ^^^^
RuntimeError: async generator ignored GeneratorExit
```

Этот пример можно исправить так:

```python
import asyncio

async def agenfn():
    try:
        yield 10
    finally:
        await asyncio.sleep(0)

async def main():
    agen = agenfn()
    print(await anext(agen))
    del agen

asyncio.run(main())
```

### Избегайте одновременных итерации и закрытия одного и того же генератора

Асинхронные генераторы могут быть повторно вызваны во время выполнения другого вызова [`__anext__()`](https://python-all.ru/3.14/reference/expressions.html#agen.__anext__) / [`athrow()`](https://python-all.ru/3.14/reference/expressions.html#agen.athrow) / [`aclose()`](https://python-all.ru/3.14/reference/expressions.html#agen.aclose). Это может привести к несогласованному состоянию async-генератора и вызвать ошибки.

Рассмотрим следующий пример:

```python
import asyncio

async def consumer():
    for idx in range(100):
        await asyncio.sleep(0)
        message = yield idx
        print('received', message)

async def amain():
    agenerator = consumer()
    await agenerator.asend(None)

    fa = asyncio.create_task(agenerator.asend('A'))
    fb = asyncio.create_task(agenerator.asend('B'))
    await fa
    await fb

asyncio.run(amain())
```

Вывод:

```python
received A
Traceback (most recent call last):
  File "test.py", line 38, in <module>
    asyncio.run(amain())
    ~~~~~~~~~~~^^^^^^^^^
  File "Lib/asyncio/runners.py", line 204, in run
    return runner.run(main)
           ~~~~~~~~~~^^^^^^
  File "Lib/asyncio/runners.py", line 127, in run
    return self._loop.run_until_complete(task)
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^
  File "Lib/asyncio/base_events.py", line 719, in run_until_complete
    return future.result()
           ~~~~~~~~~~~~~^^
  File "test.py", line 36, in amain
    await fb
RuntimeError: anext(): asynchronous generator is already running
```

Поэтому рекомендуется избегать использования асинхронных генераторов в параллельных задачах или в разных циклах событий.
