3.1 Модуль asyncio

Свої потоки для асинхронних підзадач вже давно ніхто не створює. Вірніше, створювати їх можна, але такі дії вважаються занадто низькорівневими і використовуються лише розробниками фреймворків. І то, коли без них зовсім не обійтись.

Зараз у моді асинхронне програмування, оператори async/await і корутини з тасками. Але про все по порядку…

Трохи історії

Спочатку в Python для вирішення задач асинхронного програмування використовувалися корутини, засновані на генераторах. Потім, у Python 3.4, з'явився модуль asyncio (іноді його назву записують як async IO), в якому реалізовані механізми асинхронного програмування. У Python 3.5 з’явилася конструкція async/await.

А тепер трохи вступної інформації. Спочатку я розповім коротко про всі ці речі, потім детальніше, а потім ще детальніше. По-іншому не вийде, так як майже всі вони працюють в зв'язці, і детально пояснити роботу однієї сутності без посилань на інші не вийде.

Модуль asyncio

Модуль asyncio призначений для написання асинхронних програм, забезпечуючи можливість виконання задач паралельно. Він підтримує асинхронні операції вводу-виводу, таймери, сокети, виконання корутин і багатопоточність, працюючи в одному або кількох циклах подій.

Корутини (Coroutines)

Корутини — це асинхронні функції, визначені за допомогою ключового слова async def. Корутини дозволяють призупиняти своє виконання за допомогою ключового слова await, що дозволяє іншим корутинам виконуватися в цей час.


import asyncio

# Визначення асинхронної функції (корутини)
async def main():
    print('Hello ...')
    # Призупиняємо виконання на 1 секунду
    await asyncio.sleep(1)
    print('... World!')

# Запуск асинхронної функції main() в циклі подій
asyncio.run(main())

Цикл подій (Event Loop)

Цикл подій керує виконанням корутин, задач та інших асинхронних операцій. Виклик asyncio.run() запускає цикл подій і виконує корутину до завершення.


import asyncio

async def main():
    print('Hello ...')
    await asyncio.sleep(1)
    print('... World!')

# Отримання поточного циклу подій
loop = asyncio.get_event_loop()
# Запуск корутини до завершення
loop.run_until_complete(main())
# Закриття циклу подій після завершення всіх задач
loop.close()

Задачі (Tasks)

Задачі дозволяють запускати корутини паралельно. Створюються за допомогою asyncio.create_task() або asyncio.ensure_future().


import asyncio

# Визначення корутини, яка буде виконана із затримкою
async def say_after(delay, what):
    # Призупиняємо виконання на заданий час
    await asyncio.sleep(delay)
    print(what)

# Основна корутина
async def main():
    # Створюємо задачі для паралельного виконання корутин
    task1 = asyncio.create_task(say_after(1, 'hello'))
    task2 = asyncio.create_task(say_after(2, 'world'))
    
    # Чекаємо завершення обох задач
    await task1
    await task2

# Запуск основної корутини
asyncio.run(main())

Ф'ючерси (Futures)

Об'єкти Future представляють собою результати асинхронних операцій, які будуть доступні в майбутньому. Наприклад, вони використовуються для очікування завершення асинхронного завдання.


import asyncio

# Визначення корутини, яка імітує довгу задачу
async def long_running_task():
    print('Task started')
    # Призупиняємо виконання на 3 секунди
    await asyncio.sleep(3)
    print('Task finished')
    return 'Result'

# Основна корутина
async def main():
    # Створюємо ф'ючерс для очікування завершення задачі
    future = asyncio.ensure_future(long_running_task())
    # Чекаємо завершення задачі і отримуємо результат
    result = await future  
    print(f'Task result: {result}')

# Запуск основної корутини
asyncio.run(main())

3.2 Асинхронна функція — async def

Асинхронна функція оголошується так само, як і звичайна, тільки перед ключовим словом def потрібно написати слово async.


async def Ім'яФункції(параметри):
    код функції

Асинхронна функція оголошується як звичайна, викликається як звичайна, але от результат вона повертає інший. Якщо викликати асинхронну функцію, то вона поверне не результат, а спеціальний об'єкт — корутину.

Можна навіть це перевірити:


import asyncio

async def main():
    print("Hello World")
            
# Виклик асинхронної функції, який повертає корутину
result = main()
# Перевіряємо тип результату
print(type(result)) # <class 'coroutine'>

Що ж відбувається? Коли ви позначаєте функцію словом async, то фактично додаєте до неї декоратор, який робить приблизно це:


def async_decorator(func):
    # Створюємо об'єкт Task
    task = Task()
    # Передаємо в нього нашу функцію func, щоб він її виконав
    task.target = func  
    # Додаємо об'єкт task в чергу задач — Event Loop
    eventloop.add_task(task)  
    # Повертаємо об'єкт task
    return task 

А ваш код стає схожим на:


import asyncio

@async_decorator
def main():
    print("Hello World")
            
result = main()
print(type(result)) # <class 'coroutine'>

Сенс даної аналогії наступний:

Коли ви викликаєте асинхронну функцію, створюється спеціальний об'єкт Task, який буде виконувати вашу функцію, але коли-небудь у майбутньому. Може через 0.0001 сек, а може і через 10.

Цей об'єкт task вам одразу і повертається як результат виклику вашої асинхронної функції. Це і є корутина. Ваша асинхронна функція, можливо, взагалі ще не почала виконуватися, а об'єкт task (корутина) у вас вже є.

Навіщо вам цей task (корутина)? Ви мало що можете з ним зробити, але можливо зробити 3 речі:

  • Почекати, поки асинхронна функція виконається.
  • Почекати, поки асинхронна функція закінчить виконуватися і отримати з корутини результат виконання функції.
  • Почекати, поки виконається 10 (будь-яке число) асинхронних функцій.

Як це зробити, я розповім нижче.

3.3 Оператор await

Більша частина дій з корутиною починається з «почекати виконання асинхронної функції». Тому для цього дії у нас є спеціальний оператор await.

Вам потрібно просто написати його перед корутиною:


await корутина

Або одразу перед викликом асинхронної функції:


await асинхронна_функція(аргументи)

Коли Python зустрічає у коді оператор await, він призупиняє виконання поточної функції і чекає, поки корутина не виконається — поки не завершиться асинхронна функція, на яку посилається корутина.

Важливо! Оператор await використовується лише всередині асинхронної функції для призупинення виконання до тих пір, поки не завершиться інша корутина або асинхронна операція.

Це робиться для того, щоб спростити процес перемикання між викликами асинхронних функцій. Такий виклик await — це фактично декларація «ми тут будемо чекати невідомо скільки — займіться виконанням інших асинхронних функцій».

Приклад:


import asyncio

# Визначення асинхронної функції
async def async_print(text):
    print(text)
        
# Основна асинхронна функція
async def main():
    # Використовуємо await для очікування виконання асинхронної функції
    await async_print("Hello World")
        
# Запуск основного циклу подій і виконання корутини main()
asyncio.run(main()) #запускає асинхронну функцію

Насправді оператор await працює ще хитріше — він також повертає результат виконання асинхронної функції, в якої він був викликаний.

Приклад:


import asyncio

# Визначення асинхронної функції, яка додає два числа
async def async_add(a, b):
    return a + b
        
# Основна асинхронна функція
async def main():
    # Використовуємо await для отримання результату виконання async_add
    sum = await async_add(100, 200)
    print(sum)
        
# Запуск основного циклу подій і виконання корутини main()
asyncio.run(main()) #запускає асинхронну функцію

Отже, підведемо підсумки, оператор await:

  • Призупиняє поточну асинхронну функцію до тих пір, поки не завершиться інша корутина або асинхронна операція.
  • Повертає результат виконання асинхронної операції або корутини.
  • Можна використовувати лише всередині асинхронної функції.