Розробка з asyncio

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

На цій сторінці наведено типові помилки та пастки та пояснено, як їх уникнути.

Режим налагодження

За замовчуванням asyncio працює у робочому режимі. Щоб полегшити розробку, asyncio має режим налагодження.

Є кілька способів увімкнути асинхронний режим налагодження:

Окрім увімкнення режиму налагодження, враховуйте також:

  • setting the log level of the asyncio logger to logging.DEBUG, for example the following snippet of code can be run at startup of the application:

    logging.basicConfig(level=logging.DEBUG)
    
  • налаштування модуля warnings для відображення попереджень ResourceWarning. Один із способів зробити це — скористатися параметром командного рядка -W default.

Коли ввімкнено режим налагодження:

  • asyncio перевіряє співпрограми, які не були очікувані, і записує їх у журнал; це пом’якшує помилку «забутого очікування».

  • Багато небезпечних для потоків асинхронних API (таких як методи loop.call_soon() і loop.call_at()) викликають виняток, якщо вони викликаються з неправильного потоку.

  • Час виконання селектора введення-виведення реєструється, якщо виконання операції введення-виведення займає надто багато часу.

  • Callbacks taking longer than 100 milliseconds are logged. The loop.slow_callback_duration attribute can be used to set the minimum execution duration in seconds that is considered «slow».

Паралельність і багатопотоковість

Цикл подій виконується в потоці (зазвичай головному) і виконує всі зворотні виклики та завдання у своєму потоці. Поки Завдання виконується в циклі подій, жодні інші Завдання не можуть виконуватися в тому самому потоці. Коли Завдання виконує вираз очікування, запущене Завдання призупиняється, а цикл подій виконує наступне Завдання.

Щоб запланувати callback з іншого потоку ОС, слід використовувати метод loop.call_soon_threadsafe(). Приклад:

loop.call_soon_threadsafe(callback, *args)

Майже всі асинхронні об’єкти не є потокобезпечними, що зазвичай не є проблемою, якщо немає коду, який працює з ними поза Завданням або зворотним викликом. Якщо існує потреба в такому коді для виклику низькорівневого асинхронного API, слід використовувати метод loop.call_soon_threadsafe(), наприклад:

loop.call_soon_threadsafe(fut.cancel)

Щоб запланувати об’єкт співпрограми з іншого потоку ОС, слід використати функцію run_coroutine_threadsafe(). Він повертає concurrent.futures.Future для доступу до результату:

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

# Later in another OS thread:

future = asyncio.run_coroutine_threadsafe(coro_func(), loop)
# Wait for the result:
result = future.result()

To handle signals the event loop must be run in the main thread.

Метод loop.run_in_executor() можна використовувати з concurrent.futures.ThreadPoolExecutor для виконання коду блокування в іншому потоці ОС, не блокуючи потік ОС, у якому виконується цикл подій.

There is currently no way to schedule coroutines or callbacks directly from a different process (such as one started with multiprocessing). The Методи циклу подій section lists APIs that can read from pipes and watch file descriptors without blocking the event loop. In addition, asyncio’s Subprocess APIs provide a way to start a process and communicate with it from the event loop. Lastly, the aforementioned loop.run_in_executor() method can also be used with a concurrent.futures.ProcessPoolExecutor to execute code in a different process.

Запуск коду блокування

Код блокування (прив’язаний до процесора) не слід викликати безпосередньо. Наприклад, якщо функція виконує інтенсивне обчислення ЦП протягом 1 секунди, усі одночасні асинхронні завдання та операції введення-виведення будуть відкладені на 1 секунду.

Виконавець можна використовувати для запуску завдання в іншому потоці або навіть в іншому процесі, щоб уникнути блокування потоку ОС за допомогою циклу подій. Додаткову інформацію див. у методі loop.run_in_executor().

Лісозаготівля

asyncio використовує модуль logging, і все журналювання виконується через "asyncio" реєстратор.

The default log level is logging.INFO, which can be easily adjusted:

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

Network logging can block the event loop. It is recommended to use a separate thread for handling logs or use non-blocking IO. For example, see Робота з обробниками, які блокують.

Виявляти ніколи не очікувані співпрограми

Коли функція співпрограми викликається, але не очікується (наприклад, coro() замість await coro()) або співпрограма не запланована за допомогою asyncio.create_task(), asyncio видасть RuntimeWarning:

import asyncio

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

async def main():
    test()

asyncio.run(main())

Вихід:

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

Вивід у режимі налагодження:

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():

async def main():
    await test()

Виявлення ніколи не отриманих винятків

Якщо викликається Future.set_exception(), але об’єкт Future ніколи не очікується, виняток ніколи не поширюватиметься на код користувача. У цьому випадку asyncio створюватиме повідомлення журналу, коли об’єкт Future збиратиме сміття.

Приклад необробленого винятку:

import asyncio

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

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

asyncio.run(main())

Вихід:

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

Увімкніть режим налагодження, щоб отримати відстеження місця створення завдання:

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

Вивід у режимі налагодження:

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