Перейти к содержимому

18.5.9. Разработка с asyncio

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

18.5.9.1. Режим отладки asyncio

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

Чтобы включить все проверки отладки для приложения:

  • Включите режим отладки asyncio глобально, установив переменную окружения PYTHONASYNCIODEBUG в 1 или вызвав AbstractEventLoop.set_debug().

  • Установите уровень журнала для логгера asyncio в logging.DEBUG. Например, вызовите logging.basicConfig(level=logging.DEBUG) при запуске.

  • Настройте модуль warnings для отображения предупреждений ResourceWarning. Например, используйте параметр командной строки -Wdefault Python для их отображения.

Примеры проверок отладки:

Смотрите также

Метод AbstractEventLoop.set_debug() и логгер asyncio.

18.5.9.2. Отмена

Отмена задач не распространена в классическом программировании. В асинхронном программировании это не только обычное явление, но и необходимо подготовить код к её обработке.

Future и задачи могут быть явно отменены с помощью их метода Future.cancel(). Функция wait_for() отменяет ожидаемую задачу при наступлении тайм-аута. Существует много других случаев, когда задача может быть отменена косвенно.

Не вызывайте метод set_result() или set_exception() объекта Future, если future отменён: это приведёт к исключению. Например, напишите:

python
if not fut.cancelled():
    fut.set_result('done')

Не планируйте напрямую вызов метода set_result() или set_exception() объекта future с помощью AbstractEventLoop.call_soon(): future может быть отменён до вызова его метода.

Если ожидается future, следует заранее проверить, не был ли он отменён, чтобы избежать бесполезных операций. Пример:

python
@coroutine
def slow_operation(fut):
    if fut.cancelled():
        return
    # ... slow computation ...
    yield from fut
    # ...

Функция shield() также может использоваться для игнорирования отмены.

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

Цикл событий выполняется в потоке и выполняет все колбэки и задачи в том же потоке. Пока задача выполняется в цикле событий, никакая другая задача не выполняется в том же потоке. Но когда задача использует yield from, задача приостанавливается, и цикл событий выполняет следующую задачу.

Для планирования колбэка из другого потока следует использовать метод AbstractEventLoop.call_soon_threadsafe(). Пример:

python
loop.call_soon_threadsafe(callback, *args)

Большинство объектов asyncio не являются потокобезопасными. Беспокоиться об этом стоит только при доступе к объектам вне цикла событий. Например, чтобы отменить future, не вызывайте напрямую его метод Future.cancel(), а:

python
loop.call_soon_threadsafe(fut.cancel)

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

Чтобы запланировать корутину из другого потока, следует использовать функцию run_coroutine_threadsafe(). Она возвращает concurrent.futures.Future для доступа к результату:

python
future = asyncio.run_coroutine_threadsafe(coro_func(), loop)
result = future.result(timeout)  # Ожидать результат с таймаутом

Метод AbstractEventLoop.run_in_executor() можно использовать с исполнителем пула потоков, чтобы выполнить колбэк в другом потоке и не блокировать поток цикла событий.

Смотрите также

Раздел Примитивы синхронизации описывает способы синхронизации задач.

Раздел Подпроцессы и потоки перечисляет ограничения asyncio на запуск подпроцессов из разных потоков.

18.5.9.4. Корректная обработка блокирующих функций

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

Для сетевого взаимодействия и подпроцессов модуль asyncio предоставляет высокоуровневые API, такие как протоколы.

Исполнитель можно использовать для выполнения задачи в другом потоке или даже в другом процессе, чтобы не блокировать поток цикла событий. См. метод AbstractEventLoop.run_in_executor().

Смотрите также

Раздел Отложенные вызовы подробно описывает, как цикл событий обрабатывает время.

18.5.9.5. Логирование

Модуль asyncio записывает информацию с помощью модуля logging в регистраторе 'asyncio'.

Уровень логирования по умолчанию для модуля asynciologging.INFO. Для тех, кто не хочет такой подробности от asyncio, уровень можно изменить. Например, чтобы установить уровень logging.WARNING:

text
logging.getLogger('asyncio').setLevel(logging.WARNING)

18.5.9.6. Обнаружение корутинных объектов, которые никогда не были запланированы

Когда корутинная функция вызывается, а её результат не передаётся в ensure_future() или в метод AbstractEventLoop.create_task(), выполнение корутинного объекта никогда не будет запланировано, что, вероятно, является ошибкой. Включите режим отладки asyncio, чтобы зарегистрировать предупреждение и обнаружить её.

Пример с ошибкой:

python
import asyncio

@asyncio.coroutine
def test():
    print("never scheduled")

test()

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

python
Coroutine test() at test.py:3 was never yielded from
Coroutine object created at (most recent call last):
  File "test.py", line 7, in <module>
    test()

Исправление состоит в вызове функции ensure_future() или метода AbstractEventLoop.create_task() с корутинным объектом.

18.5.9.7. Обнаружение исключений, которые никогда не были обработаны

Python обычно вызывает sys.excepthook() для необработанных исключений. Если Future.set_exception() вызывается, но исключение никогда не обрабатывается, sys.excepthook() не вызывается. Вместо этого выдаётся запись в лог, когда future удаляется сборщиком мусора, с трассировкой стека, где было возбуждено исключение.

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

python
import asyncio

@asyncio.coroutine
def bug():
    raise Exception("not consumed")

loop = asyncio.get_event_loop()
asyncio.ensure_future(bug())
loop.run_forever()
loop.close()

Вывод:

python
Task exception was never retrieved
future: <Task finished coro=<coro() done, defined at asyncio/coroutines.py:139> exception=Exception('not consumed',)>
Traceback (most recent call last):
  File "asyncio/tasks.py", line 237, in _step
    result = next(coro)
  File "asyncio/coroutines.py", line 141, in coro
    res = func(*args, **kw)
  File "test.py", line 5, in bug
    raise Exception("not consumed")
Exception: not consumed

Включите режим отладки asyncio, чтобы получить трассировку стека, где была создана задача. Вывод в режиме отладки:

python
Task exception was never retrieved
future: <Task finished coro=<bug() done, defined at test.py:3> exception=Exception('not consumed',) created at test.py:8>
source_traceback: Object created at (most recent call last):
  File "test.py", line 8, in <module>
    asyncio.ensure_future(bug())
Traceback (most recent call last):
  File "asyncio/tasks.py", line 237, in _step
    result = next(coro)
  File "asyncio/coroutines.py", line 79, in __next__
    return next(self.gen)
  File "asyncio/coroutines.py", line 141, in coro
    res = func(*args, **kw)
  File "test.py", line 5, in bug
    raise Exception("not consumed")
Exception: not consumed

Существуют разные способы решения этой проблемы. Первый способ – вложить корутину в другую корутину и использовать классический try/except:

python
@asyncio.coroutine
def handle_exception():
    try:
        yield from bug()
    except Exception:
        print("exception consumed")

loop = asyncio.get_event_loop()
asyncio.ensure_future(handle_exception())
loop.run_forever()
loop.close()

Другой способ – использовать функцию AbstractEventLoop.run_until_complete():

python
task = asyncio.ensure_future(bug())
try:
    loop.run_until_complete(task)
except Exception:
    print("exception consumed")

Смотрите также

Метод Future.exception().

18.5.9.8. Правильное связывание корутин

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

Пример с различными ошибками с использованием asyncio.sleep() для имитации медленных операций:

python
import asyncio

@asyncio.coroutine
def create():
    yield from asyncio.sleep(3.0)
    print("(1) create file")

@asyncio.coroutine
def write():
    yield from asyncio.sleep(1.0)
    print("(2) write into file")

@asyncio.coroutine
def close():
    print("(3) close file")

@asyncio.coroutine
def test():
    asyncio.ensure_future(create())
    asyncio.ensure_future(write())
    asyncio.ensure_future(close())
    yield from asyncio.sleep(2.0)
    loop.stop()

loop = asyncio.get_event_loop()
asyncio.ensure_future(test())
loop.run_forever()
print("Pending tasks at exit: %s" % asyncio.Task.all_tasks(loop))
loop.close()

Ожидаемый вывод:

text
(1) create file
(2) write into file
(3) close file
Pending tasks at exit: set()

Фактический вывод:

text
(3) close file
(2) write into file
Pending tasks at exit: {<Task pending create() at test.py:7 wait_for=<Future pending cb=[Task._wakeup()]>>}
Task was destroyed but it is pending!
task: <Task pending create() done at test.py:5 wait_for=<Future pending cb=[Task._wakeup()]>>

Цикл остановился до завершения create(), close() был вызван до write(), хотя корутинные функции вызывались в таком порядке: create(), write(), close().

Чтобы исправить пример, задачи должны быть помечены с помощью yield from:

python
@asyncio.coroutine
def test():
    yield from asyncio.ensure_future(create())
    yield from asyncio.ensure_future(write())
    yield from asyncio.ensure_future(close())
    yield from asyncio.sleep(2.0)
    loop.stop()

Или без asyncio.ensure_future():

python
@asyncio.coroutine
def test():
    yield from create()
    yield from write()
    yield from close()
    yield from asyncio.sleep(2.0)
    loop.stop()

18.5.9.9. Ожидающая задача уничтожена

Если ожидающая задача уничтожена, выполнение её обёрнутой корутины не было завершено. Вероятно, это ошибка, поэтому записывается предупреждение.

Пример лога:

text
Task was destroyed but it is pending!
task: <Task pending coro=<kill_me() done, defined at test.py:5> wait_for=<Future pending cb=[Task._wakeup()]>>

Включите режим отладки asyncio, чтобы получить tрассировку, где была создана задача. Пример лога в режиме отладки:

text
Task was destroyed but it is pending!
source_traceback: Object created at (most recent call last):
  File "test.py", line 15, in <module>
    task = asyncio.ensure_future(coro, loop=loop)
task: <Task pending coro=<kill_me() done, defined at test.py:5> wait_for=<Future pending cb=[Task._wakeup()] created at test.py:7> created at test.py:15>

18.5.9.10. Закрыть транспорты и циклы событий

Когда транспорт больше не нужен, вызовите его метод close(), чтобы освободить ресурсы. Циклы событий также должны быть явно закрыты.

Если транспорт или цикл событий не закрыт явно, в его деструкторе будет выдано предупреждение ResourceWarning. По умолчанию предупреждения ResourceWarning игнорируются. В разделе Режим отладки asyncio объясняется, как их отобразить.