Сoroutines (корутини) – це особливий вид функцій у Python, які здатні зупиняти та відновлювати своє виконання. Вони лежать в основі асинхронного програмування і забезпечують неблокуюче виконання коду. Якщо звичайна функція виконується від початку до кінця без пауз, то корутина може тимчасово «призупинитись» і передати керування циклу подій (event loop), а потім продовжити роботу з того самого місця.

Основи корутин у Python

Уявімо, що програма виконує тривалу операцію – наприклад, робить HTTP-запит або читає великий файл з диска. Якщо використовувати звичайні функції, виконання «застрягне» у цьому місці, поки не надійде відповідь. Інші частини програми не зможуть працювати конкурентно (тобто виконуватись одночасно в межах одного потоку), і користувач бачитиме «підвисання».

Корутини вирішують цю проблему: вони дозволяють тимчасово віддати керування циклу подій, щоб інші задачі могли виконуватись у той час, коли одна з них чекає на результат (наприклад, на відповідь від сервера).

Технічно, корутина – це об’єкт, який створюється при виклику асинхронної функції (вона визначається за допомогою async def). У таких функціях можна використовувати оператор await для очікування інших корутин чи асинхронних операцій. Саме await і є точкою, де виконання може «призупинитись».

У Python підтримка корутин реалізована через стандартний модуль asyncio, який надає цикл подій (event loop) та набір інструментів для роботи з асинхронними завданнями: asyncio.run(), asyncio.sleep(), asyncio.gather(), asyncio.create_task() тощо.

Визначення корутин

Щоб створити асинхронну функцію (яка повертає корутину), треба оголосити її з async def:

import asyncio


async def my_coroutine():
    print('Start')
    # await "призупиняє" my_coroutine і віддає керування циклу подій
    await asyncio.sleep(1)  # імітація асинхронної I/O операції
    print('End')

Пояснення:

  • async def – повідомляє інтерпретатору, що це асинхронна функція (coroutine function).
  • await – позначає «точку призупинки». Виконання поточної корутини my_coroutine зупиниться тут, і керування буде передано циклу подій, який може запустити інші задачі. Коли asyncio.sleep(1) завершиться, цикл подій відновить my_coroutine з цього ж місця.

Такі функції відрізняються від звичайних тим, що їх виклик не виконує тіло функції одразу, а миттєво повертає об’єкт корутини (coroutine object).

Цей об’єкт сам по собі ще нічого не робить – він є, по суті, планом виконання. Його потрібно передати в цикл подій (event loop) для виконання (наприклад, asyncio.run()).

Запуск корутин

Створений об’єкт-корутина сам по собі нічого не робить. Щоб він запрацював, його потрібно «підхопити» циклом подій (event loop). У Python для цього є модуль asyncio.

1. Простий запуск через asyncio.run()

Цей спосіб використовується найчастіше для головної функції програми. Він автоматично створює новий цикл подій, виконує передану корутину і закриває цикл після її завершення:

import asyncio


async def greet(name):
    print(f'Hello, {name}!')
    await asyncio.sleep(1)
    print(f'Goodbye, {name}!')


async def main():
    await greet('Alex')


asyncio.run(main())

Тут main() – це виклик, що створює головну корутину, яку asyncio.run() запускає. Ця корутина, в свою чергу, викликає інші (greet) через await.

2. Виконання кількох корутин одночасно

Справжня сила корутин – у конкурентному виконанні. Замість того, щоб чекати завершення кожної задачі по черзі (await task1, await task2), можна запустити їх разом за допомогою asyncio.gather:

import asyncio


async def task(name, delay):
    await asyncio.sleep(delay)
    print(f'Task {name} finished after {delay} sec')


async def main():
    await asyncio.gather(
        task('A', 2),
        task('B', 1),
        task('C', 3),
    )


asyncio.run(main())

У цьому прикладі asyncio.gather отримує три об’єкти-корутини. Всі три задачі стартують конкурентно (тобто одночасно вносяться до циклу подій). task('C') має найбільшу затримку, а результат з’являється поступово, в міру їх фактичного завершення:

Task B finished after 1 sec
Task A finished after 2 sec
Task C finished after 3 sec

3. Створення задач через create_task()

Інший підхід – створити окремі «задачі» (Tasks). Task – це об’єкт, що обгортає корутину і планує її виконання у циклі подій негайно:

import asyncio


async def worker():
    print('Worker started')
    await asyncio.sleep(2)
    print('Worker finished')


async def main():
    # Плануємо worker() на виконання і одразу рухаємось далі
    task = asyncio.create_task(worker())
    await asyncio.sleep(1)
    print('Main is doing something else...')
    # Тепер чекаємо на завершення фонової задачі
    await task


asyncio.run(main())

У цьому прикладі worker() працює конкурентно з основним кодом main(). main призупиняється на 1 секунду, в той час як worker продовжує працювати у фоні:

Worker started
Main is doing something else...
Worker finished

Підсумок

Корутини роблять програми більш ефективними: вони не блокують виконання під час очікування (наприклад, I/O-операцій), а дозволяють іншим завданням працювати конкурентно. Це особливо важливо там, де потрібно обробляти багато одночасних операцій – мережеві запити, роботу з базами даних, асинхронну взаємодію з файлами.

Завдяки корутинам Python отримав ефективну модель асинхронного програмування, яка дозволяє створювати швидкі веб-сервіси, відгукливі клієнтські програми (наприклад, з графічним інтерфейсом) та інші високопродуктивні системи.


Мітки: